pax_global_header00006660000000000000000000000064144623106650014521gustar00rootroot0000000000000052 comment=0395d69359d27614de3ed0a99d311a34cb91fe73 pg-semver-0.32.1/000077500000000000000000000000001446231066500135115ustar00rootroot00000000000000pg-semver-0.32.1/Changes000066400000000000000000000274071446231066500150160ustar00rootroot00000000000000Revision history for PostgreSQL extension semver. 0.32.1 2023-08-01T23:20:31Z - Fixed compilation issue on Postgres 16. 0.32.0 2022-10-23T21:53:54Z - Add support for binary input (receive) and output (send) functions. Thanks to Anna Clemens for the pull request (#61)! 0.31.2 2021-09-28T02:03:35Z - Add an overflow check and properly compare the max size of INT32 rather than INT. Thanks to Felix Lechner for the report and Tom Lane for the C lesson (#58). 0.31.1 2021-04-27T00:10:07Z - Updated the C code to pass the correct number of arguments to `hashint2()`. Thanks to Andrew Gierth for the spot. - Fixed an error in processing the prerelease where it could sometimes incorrectly report throw an error saying "prerelease numbers can't start with 0" for prereleases with no such leading zero. 0.31.0 2020-10-17T22:30:00Z - Added a workaround for an LLVM bitcode compile error. Thanks to @mark-s-a for the report (#40). - Removed `--load-language` from the options for running the tests, as it has not been needed since 9.1, we support 9.2 and higher, and it has been removed from Postgres 13. - Fixed an a collation error on Postgres 12 and higher. Thanks to Andrew for Marc Munro for the report and to Andrew Gierth for the fix (pgxn/pgxn-manager#67). - Prerelease parts are no longer compared compared case-insensitively, but in ASCII sort order, as required by the spec. This is a breaking change in the sense that `1.0.0-rc1` will now be considered greater than `1.0.0-RC1`, rather than equivalent, but they're both still valid. See https://github.com/semver/semver/issues/176 for the relevant discussion. Thanks to Andrew Gierth for the spot! 0.30.0 2020-05-16T19:28:36Z - ***** WARNING: This release breaks compatibility with previous versions! ****** Previous versions of the semver extension incorrectly allowed some invalid prerelease and build metadata values. Details below, but *BEFORE YOU UPGRADE* we strongly recommend that you check for and repair any invalid semvers. You can find them using the official [SemVer regular expression](https://regex101.com/r/vkijKf/1/) like so: SELECT name, version FROM packages WHERE version::text !~ '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'; If no rows are returned, you should be good to go. If there are results, here are Examples of invalid semantic versions and how they should be repaired. 1.0.0-02799 -> 1.0.0-2799 1.0.0-0.02 -> 1.0.0-0.2 1.0.0-.20 -> 1.0.0-0.20 1.0.0+0+20 -> 1.0.0+0-20 or 1.0.0+0.20 1.0.0+.af -> 1.0.0+0.af or 1.0.0+af And now, back to your regularly-scheduled changes. - Fixed an error formatting prerelease parts for semvers longer than 32 bytes. Thanks to @Nemo157 for the report and suggested fix (#48). - Removed code that converted dashes to dots in prerelease parts. It had been doing so on the assumption that dashes were invalid in SemVer 1.0 and 2.0 prerelease parts, but that does not turn out to be the case. Thanks to @Nemo157 for the report (#46). - Fixed the parsing of prerelease and metadata parts to allow leading zeros for parts with non-numeric characters, e.g., `1.0.0-alpha.0a`, and to disallow parts with leading zeros and only numeric characters, e.g., `1.0.0-02799`. Thanks to @Nemo157 for the bug report, and to Joseph Donahue for the SemVer spec expertise (#45). - The metadata part may no longer contain plus signs other than the one used to start the metadata part. - The prerelease and metadata parts may no longer start with a dot. 0.22.0 2020-04-02T14:13:55Z - Fixed `get_semver_prerelease()` so that it returns only the prerelease without any build metadata. For example, for `1.0.0-1.2+1.02`, it will now return `1.2`, not `1.2+1.02`. Thanks to Artur Troian for the report (#41) and fix (#42)! 0.21.0 2020-03-21T21:56:20Z - Fixed build metadata part to allow leading zeros. In other words, `1.0.0-1.02` is not a legitimate semver, because it has a leading zero in a numeric part of the prerelease version. `1.0.0-1.2+1.02`, however, is valid, because the leading zero numeric expression is in the build metadata part, which does not evaluate as numeric, and so is allowed. This fix allows source code hashes, for example, to be used in build metadata. Thanks to Artur Troian for the report (#38) and fix (#39)! 0.20.3 2018-11-10T19:32:26Z - Fixed test failure on Postgres 11 due to a renamed column that this extension doesn't even use for testing. 0.20.2 2018-10-19T02:05:11Z - Fix Makefile bug that installed the version script with the release version instead of the extension version. Thanks again to @ninjaslikecheese for the report (#33). 0.20.1 2018-10-18T00:30:04Z - Properly incremented version to v0.20.0 in the spec file and docs. Thanks to @ninjaslikecheese for the report (#32). 0.20.0 2018-10-17T12:59:22Z - Fixed file name in the v0.17.0 upgrade script. - Added the semverrange type. - Increased the minimum required version of PostgreSQL to 9.2 for range type support. - Fixed the `Makefile` so it properly installs `semver.sql`. Thanks to Matej Marušák for the report (Issue #31). 0.17.0 2017-05-07T07:15:25Z - Fixed potential memory leak in is_semver function. Thanks to Arun Tejasvi Chaganty (Issue #29). 0.16.0 2016-12-04T00:51:19Z - Added `get_semver_*` accessor functions to retrieve the individual parts of a semver, thanks to Geoff Montee (PR #27). 0.15.0 2016-10-28T03:38:59Z - Fixed incorrect parsing of semver build metadata. - Fixed text foratting of build metadata-only semver values. - Implemented pre-release precedence scheme as per semantic versioning 2.0.0 section 11. Thanks to Sergey Kozlov for the report (Issue #23). - Added Xavier Caron and Tom Davis as maintainers. 0.14.0 2016-08-11T05:38:27Z - Updated the extension version in `semver.control` to 0.13.0, which evidently I forgot to do for that release. Thanks to Stanislav Lorenc for the report (Issue #21). 0.13.0 2016-07-30T21:06:15Z - Fixed bug where a dash (-) was disallowed within the metadata part of a semver. Thanks to @TerraTech for the report (Issue #19) and Tom Davis for the fix (PR #20). 0.12.0 2016-06-11T06:00:19Z - Fixed bug where some items described in the SemVer 2.0.0 Spec, section 10, were not properly recognized as semvers. Thanks to Xavier Caron for the fix (with tests!) (PR #17). 0.11.0 2016-05-23T20:29:49Z - Added `is_semver()`, thanks to a pull request from Xavier Caron (PR #14). - Fixed bug where version parts with non-leading 0 were considered invalid. Thanks to @JeremySTX for the report (Issue #15) and Tom Davis for the fix (PR #16). 0.10.0 2016-05-12T00:28:03Z - Updated extension to support Semantic Versioning 2.0.0, thanks to Tom Davis (PR #13). - Improved error output to mark specific bad character, where possible, thanks to Tom Davis. - BREAKING CHANGE: `semver()` and cast constructors no longer support the semver 1.0.0-beta format format "X.Y.Zpre" (that is, without the dash), since it's not compatible with the semver 2.0.0 spec. Use `to_semver()` to convert it to X.Y.Z-pre", or specify "X.Y.Z-pre" directly. 0.5.0 2014-12-06T00:29:17Z - Fixed issue where the release file could be listed twice when installing, which causes an installation failure on some versions of PostgreSQL. - Fixed a typo in the README: `make installcheck` should be run *after* `make install`. - Fixed an issue where the installer would read the distribution version rather than the extension version. The latter is requierd to tell PostgreSQL the correct version number (since the distribution version is quite different from the extension version). - Added `LICENSE` file to simplify packaging, thanks to Richard Marko, who is packaging semver for Fedora. 0.4.0 2013-06-12T06:48:33Z - Updated the `Makefile` to reflect the recommended patterns from pgsql-hackers. Thanks to Cédric Villemain for the details. - Changed the Makefile to read the distribution name and version from META.json. - Fixed the Makefile to allow `PG_CONFIG=/path/to/pg_config` to actually work. - Fixed an off-by-one bug in the parser that could add garbage characters to the end of certain semvers with patch versions. Thanks to Andrew "RhodiumToad" Gierth for pointing out the bug. - Add a macro to fetch a semver from C function arguments, and cast to `semver*` rather than `void*`. Suggested by Andrew "RhodiumToad" Gierth. - Removed unnecessary calls to `PG_FREE_IF_COPY()`. Only really needed for toastable values, and semvers are not toastable. 0.3.0 2012-11-20T19:04:30Z - Updated the parser to support an optional dash before the prerelease version, and the formatter to always emit a dash for the prerelease version. This brings it in line with the final semver 1.0.0 specification. Thanks to Pieter van de Bruggen for making it happen. 0.2.4 2012-11-02T22:32:30Z - Fixed a memory allocation bug that could cause semvers to be displayed with missing or garbage characters. Thanks to Andrew "RhodiumToad" Gierth for help tracking down and fixing the issue. 0.2.3 2011-11-11T06:55:10Z - Fixed the `Makefile` so that the documentation file should properly be found and installed by `PGXS`. 0.2.2 2011-05-12T19:01:41 - Tweaked MultiMarkdown table layout in the docuemntation so that the header row is always processed as a header row, rather than a `pre` block. - Simplified the `CREATE EXTENSION` support in the `Makefile`. 0.2.1 2011-04-20T20:37:05 - Fixed the metadata file to reflect that the "pair" extension is not included in the semver distribution. - Added abstract and doc file to the `provides` section of `META.json`. - Removed the `NO_PGXS` stuff from `Makefile`, as the PostgreSQL core team does not recommend its use outside of the core contrib extensions. - Added PostgreSQL 9.1 `CREATE EXTENSION` support, including migration from an unpackaged install via `CREATE EXTENSION semver FROM unpackaged`. - Renamed `doc/semver.md` to `doc/semver.mdd`, so that PGXN will parse it as MultiMarkdown. This will allow the tables to be properly formatted. - Removed documentation that semver is implemented as a domain. It hasn't been since 0.2.0. - Removed Unicode characters `psql` output in the documentation. 0.2.0 2011-02-05T19:32:49 - Converted to a native type implemented in C by Sam Vilain. While David was at lunch, no less. - As a consequence, `USING` is no longer required in an `ORDER BY` clause to get proper semver sort ordering. - Added casts from nuermic types. - Renamed `clean_semver()` to `to_semver()`. 0.1.0 2010-10-07 18:31:43 - Initial version. - Implementation in pure PL/pgSQL. - Included in [PGXN Manager](https://github.com/theory/pgxn-manager). - Not otherwise released. pg-semver-0.32.1/LICENSE000066400000000000000000000022651446231066500145230ustar00rootroot00000000000000Copyright (c) 2010-2022 The pg-semver Maintainers: David E. Wheeler, Sam Vilain, Tom Davis, and Xavier Caron. This module is free software; you can redistribute it and/or modify it under the [PostgreSQL License](https://www.opensource.org/licenses/postgresql). Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies. In no event shall The pg-semver Maintainers be liable to any party for direct, indirect, special, incidental, or consequential damages, including lost profits, arising out of the use of this software and its documentation, even if The pg-semver Maintainers have been advised of the possibility of such damage. The pg-semver Maintainers specifically disclaim any warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose. The software provided hereunder is on an "as is" basis, and The pg-semver Maintainers no obligations to provide maintenance, support, updates, enhancements, or modifications. pg-semver-0.32.1/META.json000066400000000000000000000025161446231066500151360ustar00rootroot00000000000000{ "name": "semver", "abstract": "A semantic version data type", "description": "A Postgres data type for the Semantic Version format with support for btree and hash indexing.", "version": "0.32.1", "maintainer": [ "David E. Wheeler ", "Sam Vilain ", "Tom Davis ", "Xavier Caron " ], "license": "postgresql", "provides": { "semver": { "abstract": "A semantic version data type", "file": "sql/semver.sql", "docfile": "doc/semver.mmd", "version": "0.32.1" } }, "prereqs": { "runtime": { "requires": { "PostgreSQL": "9.2.0" } }, "test": { "requires": { "plpgsql": 0 } } }, "resources": { "bugtracker": { "web": "https://github.com/theory/pg-semver/issues/" }, "repository": { "url": "git://github.com/theory/pg-semver.git", "web": "https://github.com/theory/pg-semver/", "type": "git" } }, "generated_by": "David E. Wheeler", "meta-spec": { "version": "1.0.0", "url": "https://pgxn.org/meta/spec.txt" }, "tags": [ "semantic version", "semver", "version", "version number" ] }pg-semver-0.32.1/Makefile000066400000000000000000000034141446231066500151530ustar00rootroot00000000000000EXTENSION = $(shell grep -m 1 '"name":' META.json | \ sed -e 's/[[:space:]]*"name":[[:space:]]*"\([^"]*\)",/\1/') EXTVERSION = $(shell grep -m 1 '[[:space:]]\{8\}"version":' META.json | \ sed -e 's/[[:space:]]*"version":[[:space:]]*"\([^"]*\)",\{0,1\}/\1/') DISTVERSION = $(shell grep -m 1 '[[:space:]]\{3\}"version":' META.json | \ sed -e 's/[[:space:]]*"version":[[:space:]]*"\([^"]*\)",\{0,1\}/\1/') DATA = $(wildcard sql/*.sql) DOCS = $(wildcard doc/*.mmd) TESTS = $(wildcard test/sql/*.sql) REGRESS = $(patsubst test/sql/%.sql,%,$(TESTS)) REGRESS_OPTS = --inputdir=test MODULES = $(patsubst %.c,%,$(wildcard src/*.c)) PG_CONFIG ?= pg_config EXTRA_CLEAN = sql/$(EXTENSION)--$(EXTVERSION).sql PG92 = $(shell $(PG_CONFIG) --version | grep -qE " 8\.| 9\.0| 9\.1" && echo no || echo yes) ifeq ($(PG92),no) $(error $(EXTENSION) requires PostgreSQL 9.2 or higher) endif PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) all: sql/$(EXTENSION)--$(EXTVERSION).sql sql/$(EXTENSION)--$(EXTVERSION).sql: sql/$(EXTENSION).sql cp $< $@ .PHONY: results results: rsync -avP --delete results/ test/expected dist: git archive --format zip --prefix=$(EXTENSION)-$(DISTVERSION)/ -o $(EXTENSION)-$(DISTVERSION).zip HEAD latest-changes.md: Changes perl -e 'while (<>) {last if /^(v?\Q${DISTVERSION}\E)/; } print "Changes for v${DISTVERSION}:\n"; while (<>) { last if /^\s*$$/; s/^\s+//; print }' Changes > $@ # Temporary fix for PostgreSQL compilation chain / llvm bug, see # https://github.com/rdkit/rdkit/issues/2192 COMPILE.cxx.bc = $(CLANG) -xc++ -Wno-ignored-attributes $(BITCODE_CPPFLAGS) $(CPPFLAGS) -emit-llvm -c %.bc : %.cpp $(COMPILE.cxx.bc) -o $@ $< $(LLVM_BINPATH)/opt -module-summary -f $@ -o $@ pg-semver-0.32.1/README.md000066400000000000000000000067271446231066500150040ustar00rootroot00000000000000semver 0.32.0 ============= [![PGXN version](https://badge.fury.io/pg/semver.svg)](https://badge.fury.io/pg/semver) [![Build Status](https://github.com/theory/pg-semver/workflows/CI/badge.svg)](https://github.com/theory/pg-semver/actions) This library contains a single PostgreSQL extension, a data type called "semver". It's an implementation of the version number format specified by the [Semantic Versioning 2.0.0 Specification](https://semver.org/spec/v2.0.0.html). Installation ------------ To build semver: make make install make installcheck If you encounter an error such as: "Makefile", line 8: Need an operator You need to use GNU make, which may well be installed on your system as `gmake`: gmake gmake install gmake installcheck If you encounter an error such as: make: pg_config: Command not found Be sure that you have `pg_config` installed and in your path. If you used a package management system such as RPM to install PostgreSQL, be sure that the `-devel` package is also installed. If necessary tell the build process where to find it: env PG_CONFIG=/path/to/pg_config make && make installcheck && make install If you encounter an error such as: ERROR: must be owner of database regression You need to run the test suite using a super user, such as the default "postgres" super user: make installcheck PGUSER=postgres Once semver is installed, you can add it to a database. If you're running PostgreSQL 9.1.0 or greater, it's a simple as connecting to a database as a super user and running: CREATE EXTENSION semver; If you've upgraded your cluster to PostgreSQL 9.1 and already had semver installed, you can upgrade it to a properly packaged extension with: CREATE EXTENSION semver FROM unpackaged; For versions of PostgreSQL less than 9.1.0, you'll need to run the installation script: psql -d mydb -f /path/to/pgsql/share/contrib/semver.sql If you want to install semver and all of its supporting objects into a specific schema, use the `PGOPTIONS` environment variable to specify the schema, like so: PGOPTIONS=--search_path=extensions psql -d mydb -f semver.sql Dependencies ------------ The `semver` data type has no dependencies other than PostgreSQL and, for testing, PL/pgSQL. Copyright and License --------------------- Copyright (c) 2010-2022 The pg-semver Maintainers: David E. Wheeler, Sam Vilain, Tom Davis, and Xavier Caron. This module is free software; you can redistribute it and/or modify it under the [PostgreSQL License](https://www.opensource.org/licenses/postgresql). Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies. In no event shall The pg-semver Maintainers be liable to any party for direct, indirect, special, incidental, or consequential damages, including lost profits, arising out of the use of this software and its documentation, even if The pg-semver Maintainers have been advised of the possibility of such damage. The pg-semver Maintainers specifically disclaim any warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose. The software provided hereunder is on an "as is" basis, and The pg-semver Maintainers no obligations to provide maintenance, support, updates, enhancements, or modifications. pg-semver-0.32.1/doc/000077500000000000000000000000001446231066500142565ustar00rootroot00000000000000pg-semver-0.32.1/doc/semver.mmd000066400000000000000000000372111446231066500162620ustar00rootroot00000000000000semver 0.32.1 ============= Synopsis -------- CREATE EXTENSION semver; SELECT '1.2.1'::semver; semver -------- 1.2.1 SELECT '1.2.0'::semver > '1.2.0-b1'::semver; ?column? ---------- t Description ----------- This library contains a single PostgreSQL extension, a semantic version data type called `semver`. It's an implementation of the version number format specified by the [Semantic Versioning 2.0.0 Specification](https://semver.org/spec/v2.0.0.html). The salient points of [the spec](https://semver.org/), for the purposes of a data type and comparison operators, are: 1. A normal version number MUST take the form X.Y.Z where X, Y, and Z are non-negative integers, and MUST NOT contain leading zeroes. X is the major version, Y is the minor version, and Z is the patch version. Each element MUST increase numerically. For instance: `1.9.0 < 1.10.0 < 1.11.0`. 2. A pre-release version MAY be denoted by appending a hyphen and a series of dot separated identifiers immediately following the patch version. Identifiers MUST comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty. Numeric identifiers MUST NOT include leading zeroes. Pre-release versions have a lower precedence than the associated normal version. A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version. Examples: `1.0.0-alpha, 1.0.0-alpha.1, 1.0.0-0.3.7, 1.0.0-x.7.z.92`. 3. Build metadata MAY be denoted by appending a plus sign and a series of dot separated identifiers immediately following the patch or pre-release version. Identifiers MUST comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty. Build metadata SHOULD be ignored when determining version precedence. Thus two versions that differ only in the build metadata, have the same precedence. Examples: `1.0.0-alpha+001, 1.0.0+20130313144700, 1.0.0-beta+exp.sha.5114f85`. 4. Precedence refers to how versions are compared to each other when ordered. Precedence MUST be calculated by separating the version into major, minor, patch and pre-release identifiers in that order (Build metadata does not figure into precedence). Precedence is determined by the first difference when comparing each of these identifiers from left to right as follows: Major, minor, and patch versions are always compared numerically. Example: `1.0.0 < 2.0.0 < 2.1.0 < 2.1.1`. When major, minor, and patch are equal, a pre-release version has lower precedence than a normal version. Example: `1.0.0-alpha < 1.0.0`. Precedence for two pre-release versions with the same major, minor, and patch version MUST be determined by comparing each dot separated identifier from left to right until a difference is found as follows: identifiers consisting of only digits are compared numerically and identifiers with letters or hyphens are compared lexically in ASCII sort order. Numeric identifiers always have lower precedence than non-numeric identifiers. A larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding identifiers are equal. Example: `1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0`. 🚨 v0.30.0 Upgrade Compatibility Warning 🚨 ------------------------------------------- Prior to v0.30.0, the semver extension incorrectly allowed some invalid prerelease and build metadata values. Details below, but *BEFORE YOU UPGRADE* from an earlier version, we strongly recommend that you check for and repair any invalid semvers. You can find them using the official [SemVer regular expression](https://regex101.com/r/vkijKf/1/) like so (replace `name`, `version`, and `packages` as appropriate for your database): SELECT name, version FROM packages WHERE version::text !~ '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'; If no rows are returned, you should be good to go. If there are results, here are Examples of invalid semantic versions and how they should be repaired. Invalid Valid SemVer ----------- ---------------------------- 1.0.0-02799 -> 1.0.0-2799 1.0.0-0.02 -> 1.0.0-0.2 1.0.0-.20 -> 1.0.0-0.20 1.0.0+0+20 -> 1.0.0+0-20 or 1.0.0+0.20 1.0.0+.af -> 1.0.0+0.af or 1.0.0+af Usage ----- Add the extension to a database: CREATE EXTENSION semver; Now, use it like any other data type. Here's an example in a table: CREATE TABLE extensions ( name TEXT, version SEMVER, description TEXT, PRIMARY KEY (name, version) ); The type can be in indexed using btree or hash indexes: CREATE INDEX idx_extension_version ON extensions(version); CREATE INDEX hdx_extension_version ON extensions USING hash (version); Hash indexes aren't worth much, but the functionality is there to support hash aggregates in query optimizations. And some sample usage: INSERT INTO extensions VALUES ('pgtap', '0.35.0', 'PostgreSQL unit testing'), ('pgtap', '0.35.0-b1', 'PostgreSQL unit testing.'), ('pair', '0.1.0', 'Key/value pair data type'), ('PostGIS', '1.5.0', 'Gelocation data types'); SELECT * FROM extensions WHERE VERSION = '1.5.0'; name │ version │ description ---------+---------+----------------------- PostGIS │ 1.5.0 │ Gelocation data types SELECT * FROM extensions WHERE VERSION < '0.35.0'; name │ version │ description -------+-----------+-------------------------- pgtap │ 0.35.0-b1 │ PostgreSQL unit testing. pair │ 0.1.0 │ Key/value pair data type Note that "0.35.0-b1" is less than "0.35.0", as required by the specification. Use `ORDER BY` to get more of a feel for semantic version ordering rules: SELECT version FROM extensions ORDER BY version; version ----------- 0.1.0 0.35.0-b1 0.35.0 1.5.0 SELECT version FROM extensions ORDER BY version DESC; version ----------- 1.5.0 0.35.0 0.35.0-b1 0.1.0 Interface --------- ### Operators ### Operator | Description | Example | Result ----------|-------------------------------------------|-------------------------------------|-------- `=` | Are semvers equivalent | '1.2.0'semver = '1.2.00'::semver | `t` `<>` | Are semvers different | '1.2.0'semver <> '1.2.00'::semver | `f` `<` | Is semver less than right semver | '3.4.0-b1'semver < '3.4.0'::semver | `t` `<=` | Is semver less than or equal to semver | '3.4.0-b1'semver <= '3.4.0'::semver | `t` `>` | Is semver greater than right semver | '3.4.0-b1'semver > '3.4.0'::semver | `f` `>=` | Is semver greater than or equal to semver | '3.4.0-b1'semver >= '3.4.0'::semver | `f` ### Functions ### Function | Description | Example | Result ---------------------------------|---------------------------------|-------------------------------------------|---------- `to_semver(text)` | Parse semver from text | `to_semver('1.02')` | `1.2.0` `is_semver(text)` | Test semver text | `is_semver('1.2.0')` | true `semver(text)` | Cast text to semver | `semver('1.2.1')` | `1.2.1` `semver(numeric)` | Cast numeric to semver | `semver(1.2)` | `1.2.0` `semver(real)` | Cast real to semver | `semver(12.0::real)` | `12.0.0` `semver(double precision)` | Cast double precision to semver | `semver(9.2::double precision)` | `9.2.0` `semver(integer)` | Cast integer to semver | `semver(42::integer)` | `42.0.0` `semver(bigint)` | Cast bigint to semver | `semver(19::bigint)` | `19.0.0` `semver(smallint)` | Cast smallint to semver | `semver(2::smallint)` | `2.0.0` `text(semver)` | Cast semver to text | `text('1.2.54'::semver)` | `1.2.54` `get_semver_major(semver)` | Get major version part | `get_semver_major('4.2.0')` | `4` `get_semver_minor(semver)` | Get minor version part | `get_semver_minor('4.2.0')` | `2` `get_semver_patch(semver)` | Get patch version part | `get_semver_patch('4.2.0')` | `0` `get_semver_prerelease(semver)` | Get prerelease version part | `get_semver_prerelease('2.1.0-b2+bfb13')` | `b2` Numeric casts simply extract an integer from the decimal portion, so that `1.20` and `1.02` would both be parsed as `1.2.0` (but their string equivalents would not). The difference between `semver()` and `to_semver()` is that the former requires a valid semver format, while the latter is a bit more permissive, doing its best to convert other version number formats (including the older [semver 1.0.0-beta](https://semver.org/spec/v1.0.0-beta.html) prerelease format) to semantic versions: # select to_semver('1.0'); to_semver ----------- 1.0.0 (1 row) # select to_semver('1.0beta1'); to_semver ----------- 1.0.0-beta1 (1 row) As for `is_semver()`, it returns true for a valid semver format, and false for anything else, including formats that `semver()` would convert to valid semvers. In other words, its interpretation of validity is strict. And finally, the `get_semver_*` functions all return integers except for `get_semver_prerelease()`, which returns text. ### Aggregate Functions ### The examples assume the values inserted into the `extensions` table in the above examples. Function | Return Type | Description | Example | Result ---------------|-------------|---------------------------|----------------------------------------|-------- `MIN(semver)` | `semver` | Return the lowest semver | `SELECT MIN(version) FROM extensions;` | `0.1.0` `MAX(semver)` | `semver` | Return the highest semver | `SELECT MAX(version) FROM extensions;` | `1.5.0` ### Casts ### From | To | Example | Result ------------------|--------|---------------------------------|--------- text | semver | `'1.2.1'::semver` | `1.2.1` numeric | semver | `1.2::semver` | `1.2.0` real | semver | `12.0::real::semver` | `12.0.0` double precision | semver | `9.2::double precision::semver` | `9.2.0` integer | semver | `42::integer::semver` | `42.0.0` bigint | semver | `19::bigint::semver` | `19.0.0` smallint | semver | `2::smallint::semver` | `2.0.0` semver | text | `'1.2.54'::semver::text` | `1.2.54` Note that numeric casts simply extract an integer from the decimal portion, so that `1.20` and `1.02` would both be parsed as `1.2.0` (but their string equivalents would not). ### Range Type ### As of v0.20.0, the semver extension includes the `semverrange` type, which simply builds on the [range type](https://www.postgresql.org/docs/current/static/rangetypes.html) support on PostgreSQL 9.2 and higher. This allows for easy specification of ranges of semantic versions. Some examples: Range | Description -----------------------|----------------------------------- `['1.0.0', '2.0.0']` | 1.0.0 inclusive - 2.0.0 inclusive `['1.0.0', '2.0.0')` | 1.0.0 inclusive - 2.0.0 exclusive `('1.0.0', '2.0.0')` | 1.0.0 exclusive - 2.0.0 exclusive `['1.0.0',]`. | 1.0.0 inclusive - infinity The cool thing is that you can use any of the [range operators](https://www.postgresql.org/docs/current/static/functions-range.html), including the "contains" operators: For example, to see if `1.0.5` falls falls within the range `1.0.0` - `2.0.0` exclusive, run a query like this: SELECT '1.0.5'::semver <@ '[1.0.0, 2.0.0)'::semverrange; ?column? ---------- t The `semverrange` constructor will build the same range, SELECT semverrange('1.0.0', '2.0.0') @> '2.0.0'::semver; ?column? ---------- f SELECT semverrange('1.0.0', '2.0.0') @> '1.9999.9999'::semver; ?column? ---------- t Pass the optional third argument to determine the bounds inclusiveness: SELECT semverrange('1.0.0', '2.0.0', '[]') @> '2.0.0'::semver; ?column? ---------- t It works for unlimited bound, as well. For example, this query ensure that a semver is greater than or equal `1.0.0`: SELECT '1000.0.0'::semver <@ '[1.0.0,]'::semverrange; ?column? ---------- t If you need to omit some values, you can use an array of semverrange values. For example, say you want to check require a version greater than `1.0.0` and less than `2.0.0`, but versions `1.2.3` and `1.4.5` have such serious bugs that you don't want to include them. We create three ranges that use exclusive bounds to omit those versions, like so: '{"(1.0.0,1.2.3)", "(1.2.3,1.4.5)", "(1.4.5,2.0.0)"}'::semverrange[] Here's an sample how to query such an array of semverranges. SELECT version, version <@ ANY( '{"(1.0.0,1.2.3)", "(1.2.3,1.4.5)", "(1.4.5,2.0.0)"}'::semverrange[] ) AS valid FROM (VALUES ('1.0.0'::semver), ('1.0.1'), ('1.2.3'), ('1.2.4'), ('1.4.4'), ('1.4.5'), ('1.7.0'), ('2.0.0') ) AS v(version) version | valid ---------+------- 1.0.0 | f 1.0.1 | t 1.2.3 | f 1.2.4 | t 1.4.4 | t 1.4.5 | f 1.7.0 | t 2.0.0 | f Support ------- This library is stored in an open [GitHub repository](https://github.com/theory/pg-semver). Feel free to fork and contribute! Please file bug reports via [GitHub Issues](https://github.com/theory/pg-semver/issues/). Authors ------- * [David E. Wheeler](https://justatheory.com/) * [Sam Vilain](http://sam.vilain.net/) * [Tom Davis](https://github.com/tdavis) * [Xavier Caron](https://github.com/maspalio) Copyright and License --------------------- Copyright (c) 2010-2022 The pg-semver Maintainers: David E. Wheeler, Sam Vilain, Tom Davis, and Xavier Caron. This module is free software; you can redistribute it and/or modify it under the [PostgreSQL License](https://www.opensource.org/licenses/postgresql). Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies. In no event shall The pg-semver Maintainers be liable to any party for direct, indirect, special, incidental, or consequential damages, including lost profits, arising out of the use of this software and its documentation, even if The pg-semver Maintainers have been advised of the possibility of such damage. The pg-semver Maintainers specifically disclaim any warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose. The software provided hereunder is on an "as is" basis, and The pg-semver Maintainers no obligations to provide maintenance, support, updates, enhancements, or modifications. pg-semver-0.32.1/semver.control000066400000000000000000000002131446231066500164100ustar00rootroot00000000000000# semver extension comment = 'Semantic version data type' default_version = '0.32.1' module_pathname = '$libdir/semver' relocatable = true pg-semver-0.32.1/sql/000077500000000000000000000000001446231066500143105ustar00rootroot00000000000000pg-semver-0.32.1/sql/semver--0.10.0--0.11.0.sql000066400000000000000000000001441446231066500177100ustar00rootroot00000000000000CREATE OR REPLACE FUNCTION is_semver(text) RETURNS bool AS 'semver' LANGUAGE C STRICT IMMUTABLE; pg-semver-0.32.1/sql/semver--0.11.0--0.12.0.sql000066400000000000000000000000571446231066500177150ustar00rootroot00000000000000-- C source code changes only; No SQL changes. pg-semver-0.32.1/sql/semver--0.12.0--0.13.0.sql000066400000000000000000000000571446231066500177170ustar00rootroot00000000000000-- C source code changes only; No SQL changes. pg-semver-0.32.1/sql/semver--0.13.0--0.15.0.sql000066400000000000000000000000571446231066500177220ustar00rootroot00000000000000-- C source code changes only; No SQL changes. pg-semver-0.32.1/sql/semver--0.15.0--0.16.0.sql000066400000000000000000000007561446231066500177330ustar00rootroot00000000000000-- -- Accessor functions -- CREATE OR REPLACE FUNCTION get_semver_major(semver) RETURNS int4 AS 'semver' LANGUAGE C STRICT IMMUTABLE; CREATE OR REPLACE FUNCTION get_semver_minor(semver) RETURNS int4 AS 'semver' LANGUAGE C STRICT IMMUTABLE; CREATE OR REPLACE FUNCTION get_semver_patch(semver) RETURNS int4 AS 'semver' LANGUAGE C STRICT IMMUTABLE; CREATE OR REPLACE FUNCTION get_semver_prerelease(semver) RETURNS text AS 'semver' LANGUAGE C STRICT IMMUTABLE; pg-semver-0.32.1/sql/semver--0.16.0--0.17.0.sql000066400000000000000000000000571446231066500177270ustar00rootroot00000000000000-- C source code changes only; No SQL changes. pg-semver-0.32.1/sql/semver--0.17.0--0.20.0.sql000066400000000000000000000000651446231066500177210ustar00rootroot00000000000000CREATE TYPE semverrange AS RANGE (SUBTYPE = semver); pg-semver-0.32.1/sql/semver--0.2.1--0.2.4.sql000066400000000000000000000000531446231066500175550ustar00rootroot00000000000000-- No changes, all in the shared library. pg-semver-0.32.1/sql/semver--0.2.4--0.3.0.sql000066400000000000000000000000531446231066500175550ustar00rootroot00000000000000-- No changes, all in the shared library. pg-semver-0.32.1/sql/semver--0.20.0--0.21.0.sql000066400000000000000000000000571446231066500177150ustar00rootroot00000000000000-- C source code changes only; No SQL changes. pg-semver-0.32.1/sql/semver--0.21.0--0.22.0.sql000066400000000000000000000000571446231066500177170ustar00rootroot00000000000000-- C source code changes only; No SQL changes. pg-semver-0.32.1/sql/semver--0.22.0--0.30.0.sql000066400000000000000000000000571446231066500177170ustar00rootroot00000000000000-- C source code changes only; No SQL changes. pg-semver-0.32.1/sql/semver--0.3.0--0.4.0.sql000066400000000000000000000000531446231066500175530ustar00rootroot00000000000000-- No changes, all in the shared library. pg-semver-0.32.1/sql/semver--0.30.0--0.31.0.sql000066400000000000000000000000571446231066500177170ustar00rootroot00000000000000-- C source code changes only; No SQL changes. pg-semver-0.32.1/sql/semver--0.31.0--0.31.1.sql000066400000000000000000000000571446231066500177210ustar00rootroot00000000000000-- C source code changes only; No SQL changes. pg-semver-0.32.1/sql/semver--0.31.1--0.31.2.sql000066400000000000000000000000571446231066500177230ustar00rootroot00000000000000-- C source code changes only; No SQL changes. pg-semver-0.32.1/sql/semver--0.31.2--0.32.0.sql000066400000000000000000000004541446231066500177240ustar00rootroot00000000000000CREATE OR REPLACE FUNCTION semver_recv(internal) RETURNS semver AS 'semver' LANGUAGE C STRICT IMMUTABLE; CREATE OR REPLACE FUNCTION semver_send(semver) RETURNS bytea AS 'semver' LANGUAGE C STRICT IMMUTABLE; ALTER TYPE semver SET RECEIVE = semver_recv, SEND = semver_send; pg-semver-0.32.1/sql/semver--0.5.0--0.10.0.sql000066400000000000000000000000521446231066500176310ustar00rootroot00000000000000-- No changes, all in the shared library. pg-semver-0.32.1/sql/semver--unpackaged--0.2.1.sql000066400000000000000000000043051446231066500212220ustar00rootroot00000000000000ALTER EXTENSION semver ADD TYPE semver; ALTER EXTENSION semver ADD FUNCTION semver_in(cstring); ALTER EXTENSION semver ADD function semver_out(semver); ALTER EXTENSION semver ADD FUNCTION to_semver(text); ALTER EXTENSION semver ADD FUNCTION semver(text); ALTER EXTENSION semver ADD FUNCTION text(semver); ALTER EXTENSION semver ADD FUNCTION semver(numeric); ALTER EXTENSION semver ADD FUNCTION semver(real); ALTER EXTENSION semver ADD FUNCTION semver(double precision); ALTER EXTENSION semver ADD FUNCTION semver(integer); ALTER EXTENSION semver ADD FUNCTION semver(smallint); ALTER EXTENSION semver ADD FUNCTION semver(bigint); ALTER EXTENSION semver ADD CAST (semver AS text); ALTER EXTENSION semver ADD CAST (text AS semver); ALTER EXTENSION semver ADD CAST (numeric AS semver); ALTER EXTENSION semver ADD CAST (real AS semver); ALTER EXTENSION semver ADD CAST (double precision AS semver); ALTER EXTENSION semver ADD CAST (integer AS semver); ALTER EXTENSION semver ADD CAST (smallint AS semver); ALTER EXTENSION semver ADD CAST (bigint AS semver); ALTER EXTENSION semver ADD FUNCTION semver_eq(semver, semver); ALTER EXTENSION semver ADD FUNCTION semver_ne(semver, semver); ALTER EXTENSION semver ADD FUNCTION semver_le(semver, semver); ALTER EXTENSION semver ADD FUNCTION semver_lt(semver, semver); ALTER EXTENSION semver ADD FUNCTION semver_ge(semver, semver); ALTER EXTENSION semver ADD FUNCTION semver_gt(semver, semver); ALTER EXTENSION semver ADD OPERATOR =(semver,semver); ALTER EXTENSION semver ADD OPERATOR <>(semver,semver); ALTER EXTENSION semver ADD OPERATOR >(semver,semver); ALTER EXTENSION semver ADD OPERATOR >=(semver,semver); ALTER EXTENSION semver ADD OPERATOR <(semver,semver); ALTER EXTENSION semver ADD OPERATOR <=(semver,semver); ALTER EXTENSION semver ADD FUNCTION semver_cmp(semver, semver); ALTER EXTENSION semver ADD FUNCTION hash_semver(semver); ALTER EXTENSION semver ADD OPERATOR CLASS semver_ops USING btree; ALTER EXTENSION semver ADD OPERATOR CLASS semver_ops USING hash; ALTER EXTENSION semver ADD FUNCTION semver_smaller(semver, semver); ALTER EXTENSION semver ADD FUNCTION semver_larger(semver, semver); ALTER EXTENSION semver ADD AGGREGATE min(semver); ALTER EXTENSION semver ADD AGGREGATE max(semver); pg-semver-0.32.1/sql/semver.sql000066400000000000000000000154321446231066500163370ustar00rootroot00000000000000SET client_min_messages TO warning; SET log_min_messages TO warning; -- Create a semantic version data type. -- -- https://semver.org/. -- -- 1. A normal version number MUST take the form X.Y.Z where X, Y, and Z are -- integers. X is the major version, Y is the minor version, and Z is the -- patch version. Each element MUST increase numerically. For instance: 1.9.0 -- < 1.10.0 < 1.11.0. -- -- 2. A special version number MAY be denoted by appending an arbitrary string -- immediately following the patch version. The string MUST be comprised of -- only alphanumerics plus dash [0-9A-Za-z-] and MUST begin with an alpha -- character [A-Za-z]. Special versions satisfy but have a lower precedence -- than the associated normal version. Precedence SHOULD be determined by -- lexicographic ASCII sort order. For instance: 1.0.0beta1 < 1.0.0beta2 < -- 1.0.0. CREATE TYPE semver; -- -- essential IO -- CREATE OR REPLACE FUNCTION semver_in(cstring) RETURNS semver AS 'semver' LANGUAGE C STRICT IMMUTABLE; CREATE OR REPLACE FUNCTION semver_out(semver) RETURNS cstring AS 'semver' LANGUAGE C STRICT IMMUTABLE; CREATE OR REPLACE FUNCTION semver_recv(internal) RETURNS semver AS 'semver' LANGUAGE C STRICT IMMUTABLE; CREATE OR REPLACE FUNCTION semver_send(semver) RETURNS bytea AS 'semver' LANGUAGE C STRICT IMMUTABLE; -- -- The type itself. -- CREATE TYPE semver ( INPUT = semver_in, OUTPUT = semver_out, RECEIVE = semver_recv, SEND = semver_send, -- values of passedbyvalue and alignment are copied from the named type. STORAGE = plain, INTERNALLENGTH = variable, -- string category, to automatically try string conversion, etc. CATEGORY = 'S', PREFERRED = false ); -- -- A lax constructor function. -- CREATE OR REPLACE FUNCTION to_semver(text) RETURNS semver AS 'semver' LANGUAGE C STRICT IMMUTABLE; -- -- Typecasting functions. -- CREATE OR REPLACE FUNCTION semver(text) RETURNS semver AS 'semver', 'text_to_semver' LANGUAGE C STRICT IMMUTABLE; CREATE OR REPLACE FUNCTION text(semver) RETURNS text AS 'semver', 'semver_to_text' LANGUAGE C STRICT IMMUTABLE; CREATE OR REPLACE FUNCTION semver(numeric) RETURNS semver AS $$ SELECT to_semver($1::text) $$ LANGUAGE SQL STRICT IMMUTABLE; CREATE OR REPLACE FUNCTION semver(real) RETURNS semver AS $$ SELECT to_semver($1::text) $$ LANGUAGE SQL STRICT IMMUTABLE; CREATE OR REPLACE FUNCTION semver(double precision) RETURNS semver AS $$ SELECT to_semver($1::text) $$ LANGUAGE SQL STRICT IMMUTABLE; CREATE OR REPLACE FUNCTION semver(integer) RETURNS semver AS $$ SELECT to_semver($1::text) $$ LANGUAGE SQL STRICT IMMUTABLE; CREATE OR REPLACE FUNCTION semver(smallint) RETURNS semver AS $$ SELECT to_semver($1::text) $$ LANGUAGE SQL STRICT IMMUTABLE; CREATE OR REPLACE FUNCTION semver(bigint) RETURNS semver AS $$ SELECT to_semver($1::text) $$ LANGUAGE SQL STRICT IMMUTABLE; -- -- Explicit type casts. -- CREATE CAST (semver AS text) WITH FUNCTION text(semver); CREATE CAST (text AS semver) WITH FUNCTION semver(text); CREATE CAST (numeric AS semver) WITH FUNCTION semver(numeric); CREATE CAST (real AS semver) WITH FUNCTION semver(real); CREATE CAST (double precision AS semver) WITH FUNCTION semver(double precision); CREATE CAST (integer AS semver) WITH FUNCTION semver(integer); CREATE CAST (smallint AS semver) WITH FUNCTION semver(smallint); CREATE CAST (bigint AS semver) WITH FUNCTION semver(bigint); -- -- Comparison functions and their corresponding operators. -- CREATE OR REPLACE FUNCTION semver_eq(semver, semver) RETURNS bool AS 'semver' LANGUAGE C STRICT IMMUTABLE; CREATE OPERATOR = ( leftarg = semver, rightarg = semver, negator = <>, procedure = semver_eq, restrict = eqsel, commutator = =, join = eqjoinsel, hashes, merges ); CREATE OR REPLACE FUNCTION semver_ne(semver, semver) RETURNS bool AS 'semver' LANGUAGE C STRICT IMMUTABLE; CREATE OPERATOR <> ( leftarg = semver, rightarg = semver, negator = =, procedure = semver_ne, restrict = neqsel, join = neqjoinsel ); CREATE OR REPLACE FUNCTION semver_le(semver, semver) RETURNS bool AS 'semver' LANGUAGE C STRICT IMMUTABLE; CREATE OPERATOR <= ( leftarg = semver, rightarg = semver, negator = >, procedure = semver_le ); CREATE OR REPLACE FUNCTION semver_lt(semver, semver) RETURNS bool AS 'semver' LANGUAGE C STRICT IMMUTABLE; CREATE OPERATOR < ( leftarg = semver, rightarg = semver, negator = >=, procedure = semver_lt ); CREATE OR REPLACE FUNCTION semver_ge(semver, semver) RETURNS bool AS 'semver' LANGUAGE C STRICT IMMUTABLE; CREATE OPERATOR >= ( leftarg = semver, rightarg = semver, negator = <, procedure = semver_ge ); CREATE OR REPLACE FUNCTION semver_gt(semver, semver) RETURNS bool AS 'semver' LANGUAGE C STRICT IMMUTABLE; CREATE OPERATOR > ( leftarg = semver, rightarg = semver, negator = <=, procedure = semver_gt ); -- -- Support functions for indexing. -- CREATE OR REPLACE FUNCTION semver_cmp(semver, semver) RETURNS int4 AS 'semver' LANGUAGE C STRICT IMMUTABLE; CREATE OR REPLACE FUNCTION hash_semver(semver) RETURNS int4 AS 'semver' LANGUAGE C STRICT IMMUTABLE; -- -- The btree indexing operator class. -- CREATE OPERATOR CLASS semver_ops DEFAULT FOR TYPE SEMVER USING btree AS OPERATOR 1 < (semver, semver), OPERATOR 2 <= (semver, semver), OPERATOR 3 = (semver, semver), OPERATOR 4 >= (semver, semver), OPERATOR 5 > (semver, semver), FUNCTION 1 semver_cmp(semver, semver); -- -- The hash indexing operator class. -- CREATE OPERATOR CLASS semver_ops DEFAULT FOR TYPE semver USING hash AS OPERATOR 1 = (semver, semver), FUNCTION 1 hash_semver(semver); -- -- Aggregates. -- CREATE OR REPLACE FUNCTION semver_smaller(semver, semver) RETURNS semver AS 'semver' LANGUAGE C STRICT IMMUTABLE; CREATE AGGREGATE min(semver) ( SFUNC = semver_smaller, STYPE = semver, SORTOP = < ); CREATE OR REPLACE FUNCTION semver_larger(semver, semver) RETURNS semver AS 'semver' LANGUAGE C STRICT IMMUTABLE; CREATE AGGREGATE max(semver) ( SFUNC = semver_larger, STYPE = semver, SORTOP = > ); -- -- Is function. -- CREATE OR REPLACE FUNCTION is_semver(text) RETURNS bool AS 'semver' LANGUAGE C STRICT IMMUTABLE; -- -- Accessor functions -- CREATE OR REPLACE FUNCTION get_semver_major(semver) RETURNS int4 AS 'semver' LANGUAGE C STRICT IMMUTABLE; CREATE OR REPLACE FUNCTION get_semver_minor(semver) RETURNS int4 AS 'semver' LANGUAGE C STRICT IMMUTABLE; CREATE OR REPLACE FUNCTION get_semver_patch(semver) RETURNS int4 AS 'semver' LANGUAGE C STRICT IMMUTABLE; CREATE OR REPLACE FUNCTION get_semver_prerelease(semver) RETURNS text AS 'semver' LANGUAGE C STRICT IMMUTABLE; CREATE TYPE semverrange AS RANGE (SUBTYPE = semver); pg-semver-0.32.1/src/000077500000000000000000000000001446231066500143005ustar00rootroot00000000000000pg-semver-0.32.1/src/semver.c000066400000000000000000000505031446231066500157500ustar00rootroot00000000000000// -*- tab-width:4; c-basic-offset:4; indent-tabs-mode:nil; -*- /* * PostgreSQL type definitions for semver type * Written by: * + Sam Vilain * + Tom Davis * + Xavier Caron * * Copyright 2010-2022 The pg-semver Maintainers. This program is Free * Software; see the LICENSE file for the license conditions. */ #include "postgres.h" #include #include #include #include "utils/builtins.h" #include "catalog/pg_collation.h" #include "access/hash.h" #include "lib/stringinfo.h" #include "libpq/pqformat.h" #if PG_VERSION_NUM >= 160000 #include "varatt.h" #endif #ifdef PG_MODULE_MAGIC PG_MODULE_MAGIC; #endif /* IO methods */ Datum semver_in(PG_FUNCTION_ARGS); Datum semver_out(PG_FUNCTION_ARGS); Datum semver_recv(PG_FUNCTION_ARGS); Datum semver_send(PG_FUNCTION_ARGS); Datum semver_eq(PG_FUNCTION_ARGS); Datum hash_semver(PG_FUNCTION_ARGS); Datum semver_ne(PG_FUNCTION_ARGS); Datum semver_lt(PG_FUNCTION_ARGS); Datum semver_le(PG_FUNCTION_ARGS); Datum semver_ge(PG_FUNCTION_ARGS); Datum semver_gt(PG_FUNCTION_ARGS); Datum semver_cmp(PG_FUNCTION_ARGS); /* these typecasts are necessary for passing to functions that take text */ Datum text_to_semver(PG_FUNCTION_ARGS); Datum semver_to_text(PG_FUNCTION_ARGS); /* this constructor gives access to the lax parsing mode */ Datum to_semver(PG_FUNCTION_ARGS); Datum is_semver(PG_FUNCTION_ARGS); Datum semver_smaller(PG_FUNCTION_ARGS); Datum semver_larger(PG_FUNCTION_ARGS); /* these functions allow users to access individual parts of the semver */ Datum get_semver_major(PG_FUNCTION_ARGS); Datum get_semver_minor(PG_FUNCTION_ARGS); Datum get_semver_patch(PG_FUNCTION_ARGS); Datum get_semver_prerelease(PG_FUNCTION_ARGS); /* heap format of version numbers */ typedef int32 vernum; /* memory/heap structure (not for binary marshalling) */ typedef struct semver { int32 vl_len_; /* varlena header */ vernum numbers[3]; char prerel[]; /* pre-release, including the null byte for convenience */ } semver; #define PG_GETARG_SEMVER_P(n) (semver *)PG_GETARG_POINTER(n) // forward declarations, mostly to shut the compiler up but some are // actually necessary. char* emit_semver(semver* version); semver* make_semver(const int *numbers, const char* prerel); semver* parse_semver(char* str, bool lax, bool throw, bool *bad); int prerelcmp(const char* a, const char* b); int _semver_cmp(semver* a, semver* b); char* strip_meta(const char* str); int tail_cmp(char *lhs, char *rhs); semver* make_semver(const int *numbers, const char* prerel) { int varsize = offsetof(semver, prerel) + (prerel ? strlen(prerel) : 0) + 1; semver *rv = palloc(varsize); int i; SET_VARSIZE(rv, varsize); for (i = 0; i < 3; i++) { rv->numbers[i] = numbers[i]; } if (prerel) { strcpy(rv->prerel, prerel); } else { rv->prerel[0] = '\0'; } return rv; } semver* parse_semver(char* str, bool lax, bool throw, bool* bad) { int parts[] = {-1, -1, -1}; long int num; int len; int i = 0; int p = 0; int atchar = 0; int curpart = 0; char next; char* patch = 0; char* ptr, *endptr; bool dotlast = false; bool started_prerel = false; bool started_meta = false; bool skip_char = false; bool pred = false; semver* newval; *bad = false; ptr = str; len = strlen(str); do { next = (char)*ptr; skip_char = false; if (curpart < 3 && parts[2] == -1) { // Still figuring out X.Y.Z if (next == '.') { // First, check if we hit a period ptr++; atchar++; curpart++; } else { // OK, it should be a version part number then errno = 0; num = strtol(ptr, &endptr, 10); // N.B. According to strtol(3), a valid number may be preceded // by a single +/-, so a value like 0.1-1 will end up being // parsed incorrectly when in `lax` mode. It will in fact end // up being 0.0.0 because {0, 0, -1} is coerced to {0, 0, 0}. // Not fun enough? 0.0+2 becomes 0.2.0! if (ptr == endptr || next == '-' || next == '+') { // Not a number if (lax) { // Since it's not a period, we have to assume it's a legit pre- // related token. We'll skip to the next number part, but leave // the pointers. curpart++; continue; } else { *bad = true; if (!throw) break; elog(ERROR, "bad semver value '%s': expected number/separator at char %d", str, atchar); } } if (errno != 0 || num > INT32_MAX) { // Invalid or too big *bad = true; if (!throw) break; elog(ERROR, "bad semver value '%s': version number exceeds 31-bit range", str); } if (!started_meta && next == '0' && num != 0 && !lax) { // Leading zeros *bad = true; if (!throw) break; elog(ERROR, "bad semver value '%s': semver version numbers can't start with 0", str); } parts[curpart] = num; atchar += (strlen(ptr) - strlen(endptr)); ptr = endptr; } } else { // Onto pre-release/metadata if (!started_prerel && !started_meta && (next == '-' || (next != '+' && lax))) { // Starts with - started_prerel = true; if (next == '-') { skip_char = true; } } else if (next == '+') { if (started_meta) { *bad = true; elog(ERROR, "bad semver value '%s': cannot have multiple + (plus) characters in metadata", str); } else { started_meta = true; } } if (!patch && (started_meta || started_prerel)) { patch = palloc(len - atchar + 1); } if ( !skip_char && (!started_prerel && next != '-') && (!started_meta && next != '+') ) { // Didn't start with -/+ *bad = true; if (!throw) break; elog(ERROR, "bad semver value '%s': expected - (dash) or + (plus) at char %d", str, atchar); } if (next == '.' && (dotlast || (atchar + 1) == len || i == 0 || (i > 0 && patch[i-1] == '+'))) { *bad = true; if (!throw) break; elog(ERROR, "bad semver value '%s': empty pre-release section at char %d", str, atchar); } if (!skip_char && (next != '.' && next != '+' && next != '-' && !isalpha(next) && !isdigit(next))) { if (lax && isspace(next)) { // In lax mode, ignore whitespace skip_char = true; } else { *bad = true; if (!throw) break; elog(ERROR, "bad semver value '%s': non-alphanumeric pre-release at char %d", str, atchar); } } if ((started_prerel || started_meta) && !skip_char) { if (i >= 1 && (i == 1 || patch[i-2] == '.') && patch[i-1] == '0' && isdigit(next)) { pred = true; // Scan ahead. for (p = len - atchar; p < len; p++) { if (str[p] == '.') { // We got to the end of this bit. break; } if (isalpha(str[p])) { // If there is a letter, it's okay to start with a leading 0. pred = false; break; } } } if (!started_meta && (pred && !lax)) { // Leading zeros *bad = true; if (!throw) break; elog(ERROR, "bad semver value '%s': semver prerelease numbers can't start with 0", str); } else if (pred && lax) { // Swap erroneous leading zero with whatever this is patch[i-1] = next; } else { dotlast = (next == '.'); patch[i] = next; i++; } pred = false; } atchar++; ptr++; } } while (atchar < len); for (p=0; p < 3; p++) { if (parts[p] == -1) { if (lax) { parts[p] = 0; } else { *bad = true; if (!throw) break; elog(ERROR, "bad semver value '%s': missing major, minor, or patch version", str); } } } if ((started_prerel || started_meta) && i == 0) { // No pre-release value after - *bad = true; if (throw) { elog(ERROR, "bad semver value '%s': expected alphanumeric at char %d", str, atchar); } } if (started_prerel || started_meta) { patch[i] = '\0'; } newval = make_semver(parts, patch); if (patch) pfree(patch); return newval; } char* emit_semver(semver* version) { int len; char tmpbuf[32]; char *buf; if (*version->prerel == '\0') { len = snprintf( tmpbuf, sizeof(tmpbuf), "%d.%d.%d", version->numbers[0], version->numbers[1], version->numbers[2] ); } else { len = snprintf( tmpbuf, sizeof(tmpbuf),"%d.%d.%d%s%s", version->numbers[0], version->numbers[1], version->numbers[2], ((version->prerel)[0] == '+' ? "" : "-"), version->prerel ); } /* Should cover the vast majority of cases. */ if (len < sizeof(tmpbuf)) return pstrdup(tmpbuf); /* Try again, this time with the known length. */ buf = palloc(len+1); if (*version->prerel == '\0') { len = snprintf( buf, len+1, "%d.%d.%d", version->numbers[0], version->numbers[1], version->numbers[2] ); } else { len = snprintf( buf, len+1, "%d.%d.%d%s%s", version->numbers[0], version->numbers[1], version->numbers[2], ((version->prerel)[0] == '+' ? "" : "-"), version->prerel ); } return buf; } /* * Pg bindings */ /* input function: C string */ PG_FUNCTION_INFO_V1(semver_in); Datum semver_in(PG_FUNCTION_ARGS) { char *str = PG_GETARG_CSTRING(0); bool bad = false; semver *result = parse_semver(str, false, true, &bad); if (!result) PG_RETURN_NULL(); PG_RETURN_POINTER(result); }/* output function: C string */ PG_FUNCTION_INFO_V1(semver_out); Datum semver_out(PG_FUNCTION_ARGS) { semver* amount = PG_GETARG_SEMVER_P(0); char *result; result = emit_semver(amount); PG_RETURN_CSTRING(result); } /* * semver type send function * * The type is sent as text in binary mode, so this is almost the same as the * output function, but it's prefixed with a version number so we can change the * binary format sent in future if necessary. For now, only version 1 is * supported. */ PG_FUNCTION_INFO_V1(semver_send); Datum semver_send(PG_FUNCTION_ARGS) { semver *result = PG_GETARG_SEMVER_P(0); char *str = emit_semver(result); char version = 1; StringInfoData buf; pq_begintypsend(&buf); pq_sendbyte(&buf, version); pq_sendtext(&buf, str, strlen(str)); pfree(str); PG_RETURN_BYTEA_P(pq_endtypsend(&buf)); } /* * semver type recv function * * The type is sent as text in binary mode, so this is almost the same as the * input function, but it's prefixed with a version number so we can change the * binary format sent in future if necessary. For now, only version 1 is * supported. */ PG_FUNCTION_INFO_V1(semver_recv); Datum semver_recv(PG_FUNCTION_ARGS) { StringInfo buf = (StringInfo) PG_GETARG_POINTER(0); char version = pq_getmsgbyte(buf); char *str; int nbytes; bool bad = false; semver *result; if (version != 1) { elog(ERROR, "unsupported semver type version number %d", version); } str = pq_getmsgtext(buf, buf->len - buf->cursor, &nbytes); result = parse_semver(str, false, true, &bad); pfree(str); if (!result) PG_RETURN_NULL(); PG_RETURN_POINTER(result); } PG_FUNCTION_INFO_V1(text_to_semver); Datum text_to_semver(PG_FUNCTION_ARGS) { text* sv = PG_GETARG_TEXT_PP(0); bool bad = false; semver* rs = parse_semver(text_to_cstring(sv), false, true, &bad); PG_RETURN_POINTER(rs); } PG_FUNCTION_INFO_V1(semver_to_text); Datum semver_to_text(PG_FUNCTION_ARGS) { semver* sv = PG_GETARG_SEMVER_P(0); char* xxx = emit_semver(sv); text* res = cstring_to_text(xxx); pfree(xxx); PG_RETURN_TEXT_P(res); } PG_FUNCTION_INFO_V1(get_semver_major); Datum get_semver_major(PG_FUNCTION_ARGS) { semver* sv = PG_GETARG_SEMVER_P(0); int major = sv->numbers[0]; PG_RETURN_INT32(major); } PG_FUNCTION_INFO_V1(get_semver_minor); Datum get_semver_minor(PG_FUNCTION_ARGS) { semver* sv = PG_GETARG_SEMVER_P(0); int minor = sv->numbers[1]; PG_RETURN_INT32(minor); } PG_FUNCTION_INFO_V1(get_semver_patch); Datum get_semver_patch(PG_FUNCTION_ARGS) { semver* sv = PG_GETARG_SEMVER_P(0); int patch = sv->numbers[2]; PG_RETURN_INT32(patch); } PG_FUNCTION_INFO_V1(get_semver_prerelease); Datum get_semver_prerelease(PG_FUNCTION_ARGS) { semver* sv = PG_GETARG_SEMVER_P(0); char* prerelease = strip_meta(sv->prerel); text* res = cstring_to_text(prerelease); PG_RETURN_TEXT_P(res); } /* Remove everything at and after "+" in a pre-release suffix */ char* strip_meta(const char *str) { int n = strlen(str); char *copy = palloc(n + 1); int j = 0; // current character strcpy(copy, str); while (j < n) { /* if current character is b */ if (str[j] == '+') { break; } else { copy[j] = str[j]; j++; } } copy[j] = '\0'; return copy; } // https://semver.org/#spec-item-11: // Precedence for two pre-release versions with the same major, minor, and patch version MUST be determined // by comparing each dot separated identifier from left to right until a difference is found as follows: // identifiers consisting of only digits are compared numerically and identifiers with letters or hyphens // are compared lexically in ASCII sort order. Numeric identifiers always have lower precedence than // non-numeric identifiers. A larger set of pre-release fields has a higher precedence than a smaller set, // if all of the preceding identifiers are equal. Example: // 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0. #define TAIL_CMP_LT -1 #define TAIL_CMP_EQ 0 #define TAIL_CMP_GT +1 #define TAIL_CMP_KO 9 int tail_cmp ( char *lhs, char *rhs ) { char *dot = "."; char *l_last, *r_last, *l_token, *r_token; if (!strcmp(lhs, rhs)) return TAIL_CMP_EQ; l_token = strtok_r(lhs, dot, &l_last); r_token = strtok_r(rhs, dot, &r_last); if (l_token && !r_token) return TAIL_CMP_LT; if (!l_token && r_token) return TAIL_CMP_GT; while (l_token || r_token) { if (l_token && r_token) { int l_numeric = isdigit (l_token[0]); int r_numeric = isdigit (r_token[0]); if (l_numeric && r_numeric) { int l_int = atoi (l_token); int r_int = atoi (r_token); if (l_int < r_int) return TAIL_CMP_LT; if (l_int > r_int) return TAIL_CMP_GT; } else if (l_numeric) { return TAIL_CMP_LT; } else if (r_numeric) { return TAIL_CMP_GT; } else { int cmp = strcmp(l_token, r_token); if (cmp) return cmp > 0 ? TAIL_CMP_GT : TAIL_CMP_LT; } } else if (l_token) { return TAIL_CMP_GT; } else if (r_token) { return TAIL_CMP_LT; } l_token = strtok_r(NULL, dot, &l_last); r_token = strtok_r(NULL, dot, &r_last); } return TAIL_CMP_KO; } int prerelcmp(const char* a, const char* b) { int res; char *ac, *bc; ac = strip_meta(a); bc = strip_meta(b); if (*ac == '\0' && *bc != '\0') { return 1; } if (*ac != '\0' && *bc == '\0') { return -1; } res = tail_cmp(ac, bc); pfree(ac); pfree(bc); return res; } /* comparisons */ int _semver_cmp(semver* a, semver* b) { int rv, i, a_x, b_x; rv = 0; for (i = 0; i < 3; i++) { a_x = a->numbers[i]; b_x = b->numbers[i]; if (a_x < b_x) { rv = -1; break; } else if (a_x > b_x) { rv = 1; break; } } if (rv == 0) { rv = prerelcmp(a->prerel, b->prerel); } return rv; } PG_FUNCTION_INFO_V1(semver_eq); Datum semver_eq(PG_FUNCTION_ARGS) { semver* a = PG_GETARG_SEMVER_P(0); semver* b = PG_GETARG_SEMVER_P(1); int diff = _semver_cmp(a, b); PG_RETURN_BOOL(diff == 0); } PG_FUNCTION_INFO_V1(semver_ne); Datum semver_ne(PG_FUNCTION_ARGS) { semver* a = PG_GETARG_SEMVER_P(0); semver* b = PG_GETARG_SEMVER_P(1); int diff = _semver_cmp(a, b); PG_RETURN_BOOL(diff != 0); } PG_FUNCTION_INFO_V1(semver_le); Datum semver_le(PG_FUNCTION_ARGS) { semver* a = PG_GETARG_SEMVER_P(0); semver* b = PG_GETARG_SEMVER_P(1); int diff = _semver_cmp(a, b); PG_RETURN_BOOL(diff <= 0); } PG_FUNCTION_INFO_V1(semver_lt); Datum semver_lt(PG_FUNCTION_ARGS) { semver* a = PG_GETARG_SEMVER_P(0); semver* b = PG_GETARG_SEMVER_P(1); int diff = _semver_cmp(a, b); PG_RETURN_BOOL(diff < 0); } PG_FUNCTION_INFO_V1(semver_ge); Datum semver_ge(PG_FUNCTION_ARGS) { semver* a = PG_GETARG_SEMVER_P(0); semver* b = PG_GETARG_SEMVER_P(1); int diff = _semver_cmp(a, b); PG_RETURN_BOOL(diff >= 0); } PG_FUNCTION_INFO_V1(semver_gt); Datum semver_gt(PG_FUNCTION_ARGS) { semver* a = PG_GETARG_SEMVER_P(0); semver* b = PG_GETARG_SEMVER_P(1); int diff = _semver_cmp(a, b); PG_RETURN_BOOL(diff > 0); } PG_FUNCTION_INFO_V1(semver_cmp); Datum semver_cmp(PG_FUNCTION_ARGS) { semver* a = PG_GETARG_SEMVER_P(0); semver* b = PG_GETARG_SEMVER_P(1); int diff = _semver_cmp(a, b); PG_RETURN_INT32(diff); } /* so the '=' function can be 'hashes' */ PG_FUNCTION_INFO_V1(hash_semver); Datum hash_semver(PG_FUNCTION_ARGS) { semver* version = PG_GETARG_SEMVER_P(0); uint32 hash = 0; int i; Datum prerel; if (*version->prerel != '\0') { prerel = CStringGetTextDatum(version->prerel); hash = DirectFunctionCall1Coll(hashtext, C_COLLATION_OID, prerel); } for (i = 0; i < 3; i++) { hash = (hash << (7+(i<<1))) & (hash >> (25-(i<<1))); hash ^= DirectFunctionCall1(hashint2, version->numbers[i]); } PG_RETURN_INT32(hash); } PG_FUNCTION_INFO_V1(semver_larger); Datum semver_larger(PG_FUNCTION_ARGS) { semver* a = PG_GETARG_SEMVER_P(0); semver* b = PG_GETARG_SEMVER_P(1); int diff = _semver_cmp(a, b); if (diff >= 0) PG_RETURN_POINTER(a); PG_RETURN_POINTER(b); } PG_FUNCTION_INFO_V1(semver_smaller); Datum semver_smaller(PG_FUNCTION_ARGS) { semver* a = PG_GETARG_SEMVER_P(0); semver* b = PG_GETARG_SEMVER_P(1); int diff = _semver_cmp(a, b); if (diff <= 0) PG_RETURN_POINTER(a); PG_RETURN_POINTER(b); } PG_FUNCTION_INFO_V1(to_semver); Datum to_semver(PG_FUNCTION_ARGS) { text* sv = PG_GETARG_TEXT_PP(0); bool bad = false; semver* rs = parse_semver(text_to_cstring(sv), true, true, &bad); PG_RETURN_POINTER(rs); } PG_FUNCTION_INFO_V1(is_semver); Datum is_semver(PG_FUNCTION_ARGS) { text* sv = PG_GETARG_TEXT_PP(0); bool bad = false; semver* rs = parse_semver(text_to_cstring(sv), false, false, &bad); if (rs != NULL) pfree(rs); PG_RETURN_BOOL(!bad); } pg-semver-0.32.1/test/000077500000000000000000000000001446231066500144705ustar00rootroot00000000000000pg-semver-0.32.1/test/expected/000077500000000000000000000000001446231066500162715ustar00rootroot00000000000000pg-semver-0.32.1/test/expected/base.out000066400000000000000000000345731446231066500177500ustar00rootroot00000000000000\set ECHO none 1..334 ok 1 - Type semver should exist ok 2 - semvers should be NULLable ok 3 - "1.2.2" is a valid semver ok 4 - "0.2.2" is a valid semver ok 5 - "0.0.0" is a valid semver ok 6 - "0.1.999" is a valid semver ok 7 - "9999.9999999.823823" is a valid semver ok 8 - "1.0.0-beta1" is a valid semver ok 9 - "1.0.0-beta2" is a valid semver ok 10 - "1.0.0" is a valid semver ok 11 - "1.0.0-1" is a valid semver ok 12 - "1.0.0-alpha+d34dm34t" is a valid semver ok 13 - "1.0.0+d34dm34t" is a valid semver ok 14 - "20110204.0.0" is a valid semver ok 15 - "1.0.0-alpha.0a" is a valid semver ok 16 - "1.0.0+010" is a valid semver ok 17 - "1.0.0+alpha.010" is a valid semver ok 18 - "1.0.0-0AEF" is a valid semver ok 19 - "1.2" is not a valid semver ok 20 - "1.2.02" is not a valid semver ok 21 - "1.2.2-" is not a valid semver ok 22 - "1.2.3b#5" is not a valid semver ok 23 - "03.3.3" is not a valid semver ok 24 - "v1.2.2" is not a valid semver ok 25 - "1.3b" is not a valid semver ok 26 - "1.4b.0" is not a valid semver ok 27 - "1v" is not a valid semver ok 28 - "1v.2.2v" is not a valid semver ok 29 - "1.2.4b.5" is not a valid semver ok 30 - "1.0.0-alpha.010" is not a valid semver ok 31 - "1.0.0-02799" is not a valid semver ok 32 - "1.1.2+.123" is not a valid semver ok 33 - "1.1.2-.123" is not a valid semver ok 34 - "1.2.3-ñø" is not a valid semver ok 35 - "1.2.3+ñø1" is not a valid semver ok 36 - semver(1.2.2, 1.2.2) should = 0 ok 37 - v1.2.2 should = v1.2.2 ok 38 - v1.2.2 should be <= v1.2.2 ok 39 - v1.2.2 should be >= v1.2.2 ok 40 - semver(1.2.23, 1.2.23) should = 0 ok 41 - v1.2.23 should = v1.2.23 ok 42 - v1.2.23 should be <= v1.2.23 ok 43 - v1.2.23 should be >= v1.2.23 ok 44 - semver(0.0.0, 0.0.0) should = 0 ok 45 - v0.0.0 should = v0.0.0 ok 46 - v0.0.0 should be <= v0.0.0 ok 47 - v0.0.0 should be >= v0.0.0 ok 48 - semver(999.888.7777, 999.888.7777) should = 0 ok 49 - v999.888.7777 should = v999.888.7777 ok 50 - v999.888.7777 should be <= v999.888.7777 ok 51 - v999.888.7777 should be >= v999.888.7777 ok 52 - semver(0.1.2-beta3, 0.1.2-beta3) should = 0 ok 53 - v0.1.2-beta3 should = v0.1.2-beta3 ok 54 - v0.1.2-beta3 should be <= v0.1.2-beta3 ok 55 - v0.1.2-beta3 should be >= v0.1.2-beta3 ok 56 - semver(1.2.2, 1.2.3) should <> 0 ok 57 - v1.2.2 should not equal v1.2.3 ok 58 - semver(0.0.1, 1.0.0) should <> 0 ok 59 - v0.0.1 should not equal v1.0.0 ok 60 - semver(1.0.1, 1.1.0) should <> 0 ok 61 - v1.0.1 should not equal v1.1.0 ok 62 - semver(1.1.1, 1.1.0) should <> 0 ok 63 - v1.1.1 should not equal v1.1.0 ok 64 - semver(1.2.3-b, 1.2.3) should <> 0 ok 65 - v1.2.3-b should not equal v1.2.3 ok 66 - semver(1.2.3, 1.2.3-b) should <> 0 ok 67 - v1.2.3 should not equal v1.2.3-b ok 68 - semver(1.2.3-a, 1.2.3-b) should <> 0 ok 69 - v1.2.3-a should not equal v1.2.3-b ok 70 - semver(1.2.3-aaaaaaa1, 1.2.3-aaaaaaa2) should <> 0 ok 71 - v1.2.3-aaaaaaa1 should not equal v1.2.3-aaaaaaa2 ok 72 - semver(1.2.3-1.2.3, 1.2.3-1.2.3.4) should <> 0 ok 73 - v1.2.3-1.2.3 should not equal v1.2.3-1.2.3.4 ok 74 - semver(2.2.2, 1.1.1) should > 0 ok 75 - semver(1.1.1, 2.2.2) should < 0 ok 76 - v2.2.2 should be > v1.1.1 ok 77 - v2.2.2 should be >= v1.1.1 ok 78 - v1.1.1 should be < v2.2.2 ok 79 - v1.1.1 should be <= v2.2.2 ok 80 - semver(2.2.2, 2.1.1) should > 0 ok 81 - semver(2.1.1, 2.2.2) should < 0 ok 82 - v2.2.2 should be > v2.1.1 ok 83 - v2.2.2 should be >= v2.1.1 ok 84 - v2.1.1 should be < v2.2.2 ok 85 - v2.1.1 should be <= v2.2.2 ok 86 - semver(2.2.2, 2.2.1) should > 0 ok 87 - semver(2.2.1, 2.2.2) should < 0 ok 88 - v2.2.2 should be > v2.2.1 ok 89 - v2.2.2 should be >= v2.2.1 ok 90 - v2.2.1 should be < v2.2.2 ok 91 - v2.2.1 should be <= v2.2.2 ok 92 - semver(2.2.2-b, 2.2.1) should > 0 ok 93 - semver(2.2.1, 2.2.2-b) should < 0 ok 94 - v2.2.2-b should be > v2.2.1 ok 95 - v2.2.2-b should be >= v2.2.1 ok 96 - v2.2.1 should be < v2.2.2-b ok 97 - v2.2.1 should be <= v2.2.2-b ok 98 - semver(2.2.2, 2.2.2-b) should > 0 ok 99 - semver(2.2.2-b, 2.2.2) should < 0 ok 100 - v2.2.2 should be > v2.2.2-b ok 101 - v2.2.2 should be >= v2.2.2-b ok 102 - v2.2.2-b should be < v2.2.2 ok 103 - v2.2.2-b should be <= v2.2.2 ok 104 - semver(2.2.2-c, 2.2.2-b) should > 0 ok 105 - semver(2.2.2-b, 2.2.2-c) should < 0 ok 106 - v2.2.2-c should be > v2.2.2-b ok 107 - v2.2.2-c should be >= v2.2.2-b ok 108 - v2.2.2-b should be < v2.2.2-c ok 109 - v2.2.2-b should be <= v2.2.2-c ok 110 - semver(2.2.2-rc-2, 2.2.2-RC-1) should > 0 ok 111 - semver(2.2.2-RC-1, 2.2.2-rc-2) should < 0 ok 112 - v2.2.2-rc-2 should be > v2.2.2-RC-1 ok 113 - v2.2.2-rc-2 should be >= v2.2.2-RC-1 ok 114 - v2.2.2-RC-1 should be < v2.2.2-rc-2 ok 115 - v2.2.2-RC-1 should be <= v2.2.2-rc-2 ok 116 - semver(2.2.2-rc-1, 2.2.2-RC-1) should > 0 ok 117 - semver(2.2.2-RC-1, 2.2.2-rc-1) should < 0 ok 118 - v2.2.2-rc-1 should be > v2.2.2-RC-1 ok 119 - v2.2.2-rc-1 should be >= v2.2.2-RC-1 ok 120 - v2.2.2-RC-1 should be < v2.2.2-rc-1 ok 121 - v2.2.2-RC-1 should be <= v2.2.2-rc-1 ok 122 - semver(0.9.10, 0.9.9) should > 0 ok 123 - semver(0.9.9, 0.9.10) should < 0 ok 124 - v0.9.10 should be > v0.9.9 ok 125 - v0.9.10 should be >= v0.9.9 ok 126 - v0.9.9 should be < v0.9.10 ok 127 - v0.9.9 should be <= v0.9.10 ok 128 - semver(1.0.1-1.2.3, 1.0.1-0.9.9.9) should > 0 ok 129 - semver(1.0.1-0.9.9.9, 1.0.1-1.2.3) should < 0 ok 130 - v1.0.1-1.2.3 should be > v1.0.1-0.9.9.9 ok 131 - v1.0.1-1.2.3 should be >= v1.0.1-0.9.9.9 ok 132 - v1.0.1-0.9.9.9 should be < v1.0.1-1.2.3 ok 133 - v1.0.1-0.9.9.9 should be <= v1.0.1-1.2.3 ok 134 - Function to_semver() should exist ok 135 - Function to_semver(text) should exist ok 136 - Function to_semver() should return semver ok 137 - to_semver(1.2.2) should return 1.2.2 ok 138 - to_semver(01.2.2) should return 1.2.2 ok 139 - to_semver(1.02.2) should return 1.2.2 ok 140 - to_semver(1.2.02) should return 1.2.2 ok 141 - to_semver(1.2.02b) should return 1.2.2-b ok 142 - to_semver(1.2.02beta-3 ) should return 1.2.2-beta-3 ok 143 - to_semver(1.02.02rc1) should return 1.2.2-rc1 ok 144 - to_semver(1.0) should return 1.0.0 ok 145 - to_semver(1) should return 1.0.0 ok 146 - to_semver(.0.02) should return 0.0.2 ok 147 - to_semver(1..02) should return 1.0.2 ok 148 - to_semver(1..) should return 1.0.0 ok 149 - to_semver(1.1) should return 1.1.0 ok 150 - to_semver(1.2.b1) should return 1.2.0-b1 ok 151 - to_semver(9.0beta4) should return 9.0.0-beta4 ok 152 - to_semver(9b) should return 9.0.0-b ok 153 - to_semver(rc1) should return 0.0.0-rc1 ok 154 - to_semver() should return 0.0.0 ok 155 - to_semver(..2) should return 0.0.2 ok 156 - to_semver(1.2.3 a) should return 1.2.3-a ok 157 - to_semver(..2 b) should return 0.0.2-b ok 158 - to_semver( 012.2.2) should return 12.2.2 ok 159 - to_semver(20110204) should return 20110204.0.0 ok 160 - to_semver(1.0.0-alpha) should return incoming text ok 161 - to_semver(1.0.0-alpha.1) should return incoming text ok 162 - to_semver(1.0.0-0.3.7) should return incoming text ok 163 - to_semver(1.0.0-x.7.z.92) should return incoming text ok 164 - to_semver(1.0.0-alpha+001) should return incoming text ok 165 - to_semver(1.0.0+20130313144700) should return incoming text ok 166 - to_semver(1.0.0-beta+exp.sha.5114f85) should return incoming text ok 167 - "1.2.0 beta 4" is not a valid semver ok 168 - "1.2.2-" is not a valid semver ok 169 - "1.2.3b#5" is not a valid semver ok 170 - "v1.2.2" is not a valid semver ok 171 - "1.4b.0" is not a valid semver ok 172 - "1v.2.2v" is not a valid semver ok 173 - "1.2.4b.5" is not a valid semver ok 174 - "1.2.3.4" is not a valid semver ok 175 - "1.2.3 4" is not a valid semver ok 176 - "1.2000000000000000.3.4" is not a valid semver ok 177 - max(semver) should work ok 178 - min(semver) should work ok 179 - ORDER BY semver USING < should work ok 180 - ORDER BY semver USING > should work ok 181 - construct to text ok 182 - construct from text ok 183 - construct from bare number ok 184 - construct from numeric ok 185 - construct from bare integer ok 186 - construct from integer ok 187 - construct from bigint ok 188 - construct from smallint ok 189 - construct from decimal ok 190 - construct from real ok 191 - construct from double ok 192 - construct from float ok 193 - cast to text ok 194 - cast from text ok 195 - Cast from bare integer ok 196 - Cast from bare number ok 197 - Cast from numeric ok 198 - Cast from integer ok 199 - Cast from bigint ok 200 - Cast from smallint ok 201 - Cast from decimal ok 202 - Cast from decimal ok 203 - Cast from real ok 204 - Cast from double precision ok 205 - Cast from float ok 206 - Should correctly cast "1.0.0-beta" to text ok 207 - Should correctly cast "1.0.0-beta1" to text ok 208 - Should correctly cast "1.0.0-alpha" to text ok 209 - Should correctly cast "1.0.0-alph" to text ok 210 - Should correctly cast "1.0.0-food" to text ok 211 - Should correctly cast "1.0.0-f111" to text ok 212 - Should correctly cast "1.0.0-f111asbcdasdfasdfasdfasdfasdfasdffasdfadsf" to text ok 213 - "1.0.0+1" is a valid 2.0.0 semver ok 214 - "1.0.0-1+1" is a valid 2.0.0 semver ok 215 - "1.0.0-1.1+1" is a valid 2.0.0 semver ok 216 - "1.0.0-1.1.1.1.1.1.1.1.1.1.1+1.1.1.1.1.1.1.1" is a valid 2.0.0 semver ok 217 - "1.0.0-1.2" is a valid 2.0.0 semver ok 218 - "1.0.0-1.0.2" is a valid 2.0.0 semver ok 219 - "1.0.0-alpha" is a valid 2.0.0 semver ok 220 - "1.0.0-alpha.1" is a valid 2.0.0 semver ok 221 - "1.0.0-0.3.7" is a valid 2.0.0 semver ok 222 - "1.0.0-x.7.z.92" is a valid 2.0.0 semver ok 223 - "0.2.13+1583426134.07de632" is a valid 2.0.0 semver ok 224 - "1.0.0-a.." is not a valid 2.0.0 semver ok 225 - "1.0.0-a.1." is not a valid 2.0.0 semver ok 226 - "1.0.0+1_1" is not a valid 2.0.0 semver ok 227 - "1.0.0-1...." is not a valid 2.0.0 semver ok 228 - "1.0.0-1_2" is not a valid 2.0.0 semver ok 229 - "1.0.0-1.02" is not a valid 2.0.0 semver ok 230 - ORDER BY semver (2.0.0) USING < should work ok 231 - ORDER BY semver (2.0.0) USING > should work ok 232 - semver(1.0.0-1+1, 1.0.0-1+5) should = 0 ok 233 - v1.0.0-1+1 should = v1.0.0-1+5 ok 234 - v1.0.0-1+1 should be <= v1.0.0-1+5 ok 235 - v1.0.0-1+1 should be >= v1.0.0-1+5 ok 236 - semver(1.0.0-1.1+1, 1.0.0-1.1+5) should = 0 ok 237 - v1.0.0-1.1+1 should = v1.0.0-1.1+5 ok 238 - v1.0.0-1.1+1 should be <= v1.0.0-1.1+5 ok 239 - v1.0.0-1.1+1 should be >= v1.0.0-1.1+5 ok 240 - Should correctly represent "0.5.0-release1" as "0.5.0-release1" ok 241 - Should correctly represent "0.5.0release1" as "0.5.0-release1" ok 242 - Should correctly represent "0.5-release1" as "0.5.0-release1" ok 243 - Should correctly represent "0.5release1" as "0.5.0-release1" ok 244 - Should correctly represent "0.5-1" as "0.5.0-1" ok 245 - Should correctly represent "1.2.3-1.02" as "1.2.3-1.2" ok 246 - Function is_semver() should exist ok 247 - Function is_semver(text) should exist ok 248 - Function is_semver() should return boolean ok 249 - is_semver(1.2.2) should return true ok 250 - is_semver(0.2.2) should return true ok 251 - is_semver(0.0.0) should return true ok 252 - is_semver(0.1.999) should return true ok 253 - is_semver(9999.9999999.823823) should return true ok 254 - is_semver(1.0.0-beta1) should return true ok 255 - is_semver(1.0.0-beta2) should return true ok 256 - is_semver(1.0.0) should return true ok 257 - is_semver(1.0.0-1) should return true ok 258 - is_semver(1.0.0-alpha+d34dm34t) should return true ok 259 - is_semver(1.0.0+d34dm34t) should return true ok 260 - is_semver(20110204.0.0) should return true ok 261 - is_semver(1.2) should return false ok 262 - is_semver(1.2.02) should return false ok 263 - is_semver(1.2.2-) should return false ok 264 - is_semver(1.2.3b#5) should return false ok 265 - is_semver(03.3.3) should return false ok 266 - is_semver(v1.2.2) should return false ok 267 - is_semver(1.3b) should return false ok 268 - is_semver(1.4b.0) should return false ok 269 - is_semver(1v) should return false ok 270 - is_semver(1v.2.2v) should return false ok 271 - is_semver(1.2.4b.5) should return false ok 272 - is_semver(2016.5.18-MYW-600) should return true ok 273 - is_semver(1010.5.0+2016-05-27-1832) should return true ok 274 - is_semver(0.2.13+1583426134.07de632) should return true ok 275 - "2.3.0+80" is a valid semver ok 276 - to_semver(2.3.0+80) should return 2.3.0+80 ok 277 - Should correctly cast "2.3.0+80" to text ok 278 - "2.3.0+80" > "2.3.0+110" (NOT!) ok 279 - "2.3.0+80" > "2.3.0-alpha+110" ok 280 - ORDER BY semver USING < should work (section 11) ok 281 - ORDER BY semver USING > should work (section 11) ok 282 - "1.0.0" = "1.0.0+535" ok 283 - "1.0.0" < "1.0.0+535" (NOT!) ok 284 - "1.0.0" > "1.0.0+535" (NOT!) ok 285 - Function get_semver_major() should exist ok 286 - semver ok 287 - Function get_semver_major() should return integer ok 288 - major version check ok 289 - Function get_semver_minor() should exist ok 290 - semver ok 291 - Function get_semver_minor() should return integer ok 292 - minor version check ok 293 - Function get_semver_patch() should exist ok 294 - semver ok 295 - Function get_semver_patch() should return integer ok 296 - patch version check ok 297 - Function get_semver_prerelease() should exist ok 298 - semver ok 299 - Function get_semver_prerelease() should return text ok 300 - prerelease label check ok 301 - prerelease label check. must return prerelease only ok 302 - prerelease label check. must return empty string ok 303 - 1.0.0 should be in range [1.0.0, 2.0.0] ok 304 - 1.0.0 should not be in range [1.0.1, 2.0.0] ok 305 - 2.0.0 should not be in range [1.0.1, 2.0.0) ok 306 - 1.9999.9999 should be in range [1.0.1, 2.0.0) ok 307 - 1000.0.0 should be in range [1.0.0,) ok 308 - Should be able to work with arrays of semverranges ok 309 - Should properly format a 32 character semver ok 310 - Should properly format a 33 character semver ok 311 - Should propery format a prerelease with a hyphen ok 312 - Should get distinct values via hash aggregation ok 313 - Function semver_send() should exist ok 314 - Function semver_send(semver) should exist ok 315 - Function semver_send() should return bytea ok 316 - Function semver_recv() should exist ok 317 - Function semver_recv(internal) should exist ok 318 - Function semver_recv() should return semver ok 319 - semver_send('0.9.9-a1.1+1234') ok 320 - semver_send('0.9.9-a1.2.3') ok 321 - semver_send('0.9.9-a1.2') ok 322 - semver_send('0.9.9') ok 323 - semver_send('1.0.0+99') ok 324 - semver_send('1.0.0-1') ok 325 - semver_send('1.2.2') ok 326 - semver_send('9999.9999999.823823') ok 327 - semver_send('1.0.0-beta1') ok 328 - semver_send('1.0.0-1') ok 329 - semver_send('1.0.0-alpha+d34dm34t') ok 330 - semver_send('1.0.0+d34dm34t') ok 331 - semver_send('20110204.0.0') ok 332 - semver_send('1.0.0-0AEF') ok 333 - semver_send(NULL) ok 334 - Should have binary copied all of the semvers pg-semver-0.32.1/test/expected/corpus.out000066400000000000000000000071411446231066500203400ustar00rootroot00000000000000\set ECHO none 1..71 ok 1 - "0.0.4" is a valid semver ok 2 - "1.2.3" is a valid semver ok 3 - "10.20.30" is a valid semver ok 4 - "1.1.2-prerelease+meta" is a valid semver ok 5 - "1.1.2+meta" is a valid semver ok 6 - "1.1.2+meta-valid" is a valid semver ok 7 - "1.0.0-alpha" is a valid semver ok 8 - "1.0.0-beta" is a valid semver ok 9 - "1.0.0-alpha.beta" is a valid semver ok 10 - "1.0.0-alpha.beta.1" is a valid semver ok 11 - "1.0.0-alpha.1" is a valid semver ok 12 - "1.0.0-alpha0.valid" is a valid semver ok 13 - "1.0.0-alpha.0valid" is a valid semver ok 14 - "1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay" is a valid semver ok 15 - "1.0.0-rc.1+build.1" is a valid semver ok 16 - "2.0.0-rc.1+build.123" is a valid semver ok 17 - "1.2.3-beta" is a valid semver ok 18 - "10.2.3-DEV-SNAPSHOT" is a valid semver ok 19 - "1.2.3-SNAPSHOT-123" is a valid semver ok 20 - "1.0.0" is a valid semver ok 21 - "2.0.0" is a valid semver ok 22 - "1.1.7" is a valid semver ok 23 - "2.0.0+build.1848" is a valid semver ok 24 - "2.0.1-alpha.1227" is a valid semver ok 25 - "1.0.0-alpha+beta" is a valid semver ok 26 - "1.2.3----RC-SNAPSHOT.12.9.1--.12+788" is a valid semver ok 27 - "1.2.3----R-S.12.9.1--.12+meta" is a valid semver ok 28 - "1.2.3----RC-SNAPSHOT.12.9.1--.12" is a valid semver ok 29 - "1.0.0+0.build.1-rc.10000aaa-kk-0.1" is a valid semver ok 30 - "1.0.0-0A.is.legal" is a valid semver not ok 31 - "99999999999999999999999.999999999999999999.99999999999999999" is a valid semver # TODO Large versions overflow integer bounds # Failed (TODO) test 31: ""99999999999999999999999.999999999999999999.99999999999999999" is a valid semver" # died: XX000: bad semver value '99999999999999999999999.999999999999999999.99999999999999999': version number exceeds 31-bit range ok 32 - "1" is not a valid semver ok 33 - "1.2" is not a valid semver ok 34 - "1.2.3-0123" is not a valid semver ok 35 - "1.2.3-0123.0123" is not a valid semver ok 36 - "1.1.2+.123" is not a valid semver ok 37 - "+invalid" is not a valid semver ok 38 - "-invalid" is not a valid semver ok 39 - "-invalid+invalid" is not a valid semver ok 40 - "-invalid.01" is not a valid semver ok 41 - "alpha" is not a valid semver ok 42 - "alpha.beta" is not a valid semver ok 43 - "alpha.beta.1" is not a valid semver ok 44 - "alpha.1" is not a valid semver ok 45 - "alpha+beta" is not a valid semver ok 46 - "alpha_beta" is not a valid semver ok 47 - "alpha." is not a valid semver ok 48 - "alpha.." is not a valid semver ok 49 - "beta" is not a valid semver ok 50 - "1.0.0-alpha_beta" is not a valid semver ok 51 - "-alpha." is not a valid semver ok 52 - "1.0.0-alpha.." is not a valid semver ok 53 - "1.0.0-alpha..1" is not a valid semver ok 54 - "1.0.0-alpha...1" is not a valid semver ok 55 - "1.0.0-alpha....1" is not a valid semver ok 56 - "1.0.0-alpha.....1" is not a valid semver ok 57 - "1.0.0-alpha......1" is not a valid semver ok 58 - "1.0.0-alpha.......1" is not a valid semver ok 59 - "01.1.1" is not a valid semver ok 60 - "1.01.1" is not a valid semver ok 61 - "1.1.01" is not a valid semver ok 62 - "1.2" is not a valid semver ok 63 - "1.2.3.DEV" is not a valid semver ok 64 - "1.2-SNAPSHOT" is not a valid semver ok 65 - "1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788" is not a valid semver ok 66 - "1.2-RC-SNAPSHOT" is not a valid semver ok 67 - "-1.0.3-gamma+b7718" is not a valid semver ok 68 - "+justmeta" is not a valid semver ok 69 - "9.8.7+meta+meta" is not a valid semver ok 70 - "9.8.7-whatever+meta+meta" is not a valid semver ok 71 - "99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12" is not a valid semver pg-semver-0.32.1/test/pgtap-core.sql000066400000000000000000003174311446231066500172630ustar00rootroot00000000000000\set ECHO none -- This file defines pgTAP Core, a portable collection of assertion -- functions for TAP-based unit testing on PostgreSQL 8.3 or higher. It is -- distributed under the revised FreeBSD license. The home page for the pgTAP -- project is: -- -- https://pgtap.org/ -- \pset format unaligned \pset tuples_only true \pset pager -- Revert all changes on failure. \set ON_ERROR_ROLLBACK 1 \set ON_ERROR_STOP true CREATE OR REPLACE FUNCTION pg_version() RETURNS text AS 'SELECT current_setting(''server_version'')' LANGUAGE SQL IMMUTABLE; CREATE OR REPLACE FUNCTION pg_version_num() RETURNS integer AS $$ SELECT s.a[1]::int * 10000 + COALESCE(substring(s.a[2] FROM '[[:digit:]]+')::int, 0) * 100 + COALESCE(substring(s.a[3] FROM '[[:digit:]]+')::int, 0) FROM ( SELECT string_to_array(current_setting('server_version'), '.') AS a ) AS s; $$ LANGUAGE SQL IMMUTABLE; CREATE OR REPLACE FUNCTION pgtap_version() RETURNS NUMERIC AS 'SELECT 0.25;' LANGUAGE SQL IMMUTABLE; CREATE OR REPLACE FUNCTION plan( integer ) RETURNS TEXT AS $$ DECLARE rcount INTEGER; BEGIN BEGIN EXECUTE ' CREATE TEMP SEQUENCE __tcache___id_seq; CREATE TEMP TABLE __tcache__ ( id INTEGER NOT NULL DEFAULT nextval(''__tcache___id_seq''), label TEXT NOT NULL, value INTEGER NOT NULL, note TEXT NOT NULL DEFAULT '''' ); CREATE UNIQUE INDEX __tcache___key ON __tcache__(id); GRANT ALL ON TABLE __tcache__ TO PUBLIC; GRANT ALL ON TABLE __tcache___id_seq TO PUBLIC; CREATE TEMP SEQUENCE __tresults___numb_seq; CREATE TEMP TABLE __tresults__ ( numb INTEGER NOT NULL DEFAULT nextval(''__tresults___numb_seq''), ok BOOLEAN NOT NULL DEFAULT TRUE, aok BOOLEAN NOT NULL DEFAULT TRUE, descr TEXT NOT NULL DEFAULT '''', type TEXT NOT NULL DEFAULT '''', reason TEXT NOT NULL DEFAULT '''' ); CREATE UNIQUE INDEX __tresults___key ON __tresults__(numb); GRANT ALL ON TABLE __tresults__ TO PUBLIC; GRANT ALL ON TABLE __tresults___numb_seq TO PUBLIC; '; EXCEPTION WHEN duplicate_table THEN -- Raise an exception if there's already a plan. EXECUTE 'SELECT TRUE FROM __tcache__ WHERE label = ''plan'''; GET DIAGNOSTICS rcount = ROW_COUNT; IF rcount > 0 THEN RAISE EXCEPTION 'You tried to plan twice!'; END IF; END; -- Save the plan and return. PERFORM _set('plan', $1 ); RETURN '1..' || $1; END; $$ LANGUAGE plpgsql strict; CREATE OR REPLACE FUNCTION no_plan() RETURNS SETOF boolean AS $$ BEGIN PERFORM plan(0); RETURN; END; $$ LANGUAGE plpgsql strict; CREATE OR REPLACE FUNCTION _get ( text ) RETURNS integer AS $$ DECLARE ret integer; BEGIN EXECUTE 'SELECT value FROM __tcache__ WHERE label = ' || quote_literal($1) || ' LIMIT 1' INTO ret; RETURN ret; END; $$ LANGUAGE plpgsql strict; CREATE OR REPLACE FUNCTION _get_latest ( text ) RETURNS integer[] AS $$ DECLARE ret integer[]; BEGIN EXECUTE 'SELECT ARRAY[ id, value] FROM __tcache__ WHERE label = ' || quote_literal($1) || ' AND id = (SELECT MAX(id) FROM __tcache__ WHERE label = ' || quote_literal($1) || ') LIMIT 1' INTO ret; RETURN ret; END; $$ LANGUAGE plpgsql strict; CREATE OR REPLACE FUNCTION _get_latest ( text, integer ) RETURNS integer AS $$ DECLARE ret integer; BEGIN EXECUTE 'SELECT MAX(id) FROM __tcache__ WHERE label = ' || quote_literal($1) || ' AND value = ' || $2 INTO ret; RETURN ret; END; $$ LANGUAGE plpgsql strict; CREATE OR REPLACE FUNCTION _get_note ( text ) RETURNS text AS $$ DECLARE ret text; BEGIN EXECUTE 'SELECT note FROM __tcache__ WHERE label = ' || quote_literal($1) || ' LIMIT 1' INTO ret; RETURN ret; END; $$ LANGUAGE plpgsql strict; CREATE OR REPLACE FUNCTION _get_note ( integer ) RETURNS text AS $$ DECLARE ret text; BEGIN EXECUTE 'SELECT note FROM __tcache__ WHERE id = ' || $1 || ' LIMIT 1' INTO ret; RETURN ret; END; $$ LANGUAGE plpgsql strict; CREATE OR REPLACE FUNCTION _set ( text, integer, text ) RETURNS integer AS $$ DECLARE rcount integer; BEGIN EXECUTE 'UPDATE __tcache__ SET value = ' || $2 || CASE WHEN $3 IS NULL THEN '' ELSE ', note = ' || quote_literal($3) END || ' WHERE label = ' || quote_literal($1); GET DIAGNOSTICS rcount = ROW_COUNT; IF rcount = 0 THEN RETURN _add( $1, $2, $3 ); END IF; RETURN $2; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION _set ( text, integer ) RETURNS integer AS $$ SELECT _set($1, $2, '') $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _set ( integer, integer ) RETURNS integer AS $$ BEGIN EXECUTE 'UPDATE __tcache__ SET value = ' || $2 || ' WHERE id = ' || $1; RETURN $2; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION _add ( text, integer, text ) RETURNS integer AS $$ BEGIN EXECUTE 'INSERT INTO __tcache__ (label, value, note) values (' || quote_literal($1) || ', ' || $2 || ', ' || quote_literal(COALESCE($3, '')) || ')'; RETURN $2; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION _add ( text, integer ) RETURNS integer AS $$ SELECT _add($1, $2, '') $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION add_result ( bool, bool, text, text, text ) RETURNS integer AS $$ BEGIN EXECUTE 'INSERT INTO __tresults__ ( ok, aok, descr, type, reason ) VALUES( ' || $1 || ', ' || $2 || ', ' || quote_literal(COALESCE($3, '')) || ', ' || quote_literal($4) || ', ' || quote_literal($5) || ' )'; RETURN currval('__tresults___numb_seq'); END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION num_failed () RETURNS INTEGER AS $$ DECLARE ret integer; BEGIN EXECUTE 'SELECT COUNT(*)::INTEGER FROM __tresults__ WHERE ok = FALSE' INTO ret; RETURN ret; END; $$ LANGUAGE plpgsql strict; CREATE OR REPLACE FUNCTION _finish ( INTEGER, INTEGER, INTEGER) RETURNS SETOF TEXT AS $$ DECLARE curr_test ALIAS FOR $1; exp_tests INTEGER := $2; num_faild ALIAS FOR $3; plural CHAR; BEGIN plural := CASE exp_tests WHEN 1 THEN '' ELSE 's' END; IF curr_test IS NULL THEN RAISE EXCEPTION '# No tests run!'; END IF; IF exp_tests = 0 OR exp_tests IS NULL THEN -- No plan. Output one now. exp_tests = curr_test; RETURN NEXT '1..' || exp_tests; END IF; IF curr_test <> exp_tests THEN RETURN NEXT diag( 'Looks like you planned ' || exp_tests || ' test' || plural || ' but ran ' || curr_test ); ELSIF num_faild > 0 THEN RETURN NEXT diag( 'Looks like you failed ' || num_faild || ' test' || CASE num_faild WHEN 1 THEN '' ELSE 's' END || ' of ' || exp_tests ); ELSE END IF; RETURN; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION finish () RETURNS SETOF TEXT AS $$ SELECT * FROM _finish( _get('curr_test'), _get('plan'), num_failed() ); $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION diag ( msg text ) RETURNS TEXT AS $$ SELECT '# ' || replace( replace( replace( $1, E'\r\n', E'\n# ' ), E'\n', E'\n# ' ), E'\r', E'\n# ' ); $$ LANGUAGE sql strict; CREATE OR REPLACE FUNCTION diag ( msg anyelement ) RETURNS TEXT AS $$ SELECT diag($1::text); $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION diag( VARIADIC text[] ) RETURNS TEXT AS $$ SELECT diag(array_to_string($1, '')); $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION diag( VARIADIC anyarray ) RETURNS TEXT AS $$ SELECT diag(array_to_string($1, '')); $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION ok ( boolean, text ) RETURNS TEXT AS $$ DECLARE aok ALIAS FOR $1; descr text := $2; test_num INTEGER; todo_why TEXT; ok BOOL; BEGIN todo_why := _todo(); ok := CASE WHEN aok = TRUE THEN aok WHEN todo_why IS NULL THEN COALESCE(aok, false) ELSE TRUE END; IF _get('plan') IS NULL THEN RAISE EXCEPTION 'You tried to run a test without a plan! Gotta have a plan'; END IF; test_num := add_result( ok, COALESCE(aok, false), descr, CASE WHEN todo_why IS NULL THEN '' ELSE 'todo' END, COALESCE(todo_why, '') ); RETURN (CASE aok WHEN TRUE THEN '' ELSE 'not ' END) || 'ok ' || _set( 'curr_test', test_num ) || CASE descr WHEN '' THEN '' ELSE COALESCE( ' - ' || substr(diag( descr ), 3), '' ) END || COALESCE( ' ' || diag( 'TODO ' || todo_why ), '') || CASE aok WHEN TRUE THEN '' ELSE E'\n' || diag('Failed ' || CASE WHEN todo_why IS NULL THEN '' ELSE '(TODO) ' END || 'test ' || test_num || CASE descr WHEN '' THEN '' ELSE COALESCE(': "' || descr || '"', '') END ) || CASE WHEN aok IS NULL THEN E'\n' || diag(' (test result was NULL)') ELSE '' END END; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION ok ( boolean ) RETURNS TEXT AS $$ SELECT ok( $1, NULL ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION is (anyelement, anyelement, text) RETURNS TEXT AS $$ DECLARE result BOOLEAN; output TEXT; BEGIN -- Would prefer $1 IS NOT DISTINCT FROM, but that's not supported by 8.1. result := NOT $1 IS DISTINCT FROM $2; output := ok( result, $3 ); RETURN output || CASE result WHEN TRUE THEN '' ELSE E'\n' || diag( ' have: ' || CASE WHEN $1 IS NULL THEN 'NULL' ELSE $1::text END || E'\n want: ' || CASE WHEN $2 IS NULL THEN 'NULL' ELSE $2::text END ) END; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION is (anyelement, anyelement) RETURNS TEXT AS $$ SELECT is( $1, $2, NULL); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION isnt (anyelement, anyelement, text) RETURNS TEXT AS $$ DECLARE result BOOLEAN; output TEXT; BEGIN result := $1 IS DISTINCT FROM $2; output := ok( result, $3 ); RETURN output || CASE result WHEN TRUE THEN '' ELSE E'\n' || diag( ' have: ' || COALESCE( $1::text, 'NULL' ) || E'\n want: anything else' ) END; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION isnt (anyelement, anyelement) RETURNS TEXT AS $$ SELECT isnt( $1, $2, NULL); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _alike ( BOOLEAN, ANYELEMENT, TEXT, TEXT ) RETURNS TEXT AS $$ DECLARE result ALIAS FOR $1; got ALIAS FOR $2; rx ALIAS FOR $3; descr ALIAS FOR $4; output TEXT; BEGIN output := ok( result, descr ); RETURN output || CASE result WHEN TRUE THEN '' ELSE E'\n' || diag( ' ' || COALESCE( quote_literal(got), 'NULL' ) || E'\n doesn''t match: ' || COALESCE( quote_literal(rx), 'NULL' ) ) END; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION matches ( anyelement, text, text ) RETURNS TEXT AS $$ SELECT _alike( $1 ~ $2, $1, $2, $3 ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION matches ( anyelement, text ) RETURNS TEXT AS $$ SELECT _alike( $1 ~ $2, $1, $2, NULL ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION imatches ( anyelement, text, text ) RETURNS TEXT AS $$ SELECT _alike( $1 ~* $2, $1, $2, $3 ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION imatches ( anyelement, text ) RETURNS TEXT AS $$ SELECT _alike( $1 ~* $2, $1, $2, NULL ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION alike ( anyelement, text, text ) RETURNS TEXT AS $$ SELECT _alike( $1 ~~ $2, $1, $2, $3 ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION alike ( anyelement, text ) RETURNS TEXT AS $$ SELECT _alike( $1 ~~ $2, $1, $2, NULL ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION ialike ( anyelement, text, text ) RETURNS TEXT AS $$ SELECT _alike( $1 ~~* $2, $1, $2, $3 ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION ialike ( anyelement, text ) RETURNS TEXT AS $$ SELECT _alike( $1 ~~* $2, $1, $2, NULL ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _unalike ( BOOLEAN, ANYELEMENT, TEXT, TEXT ) RETURNS TEXT AS $$ DECLARE result ALIAS FOR $1; got ALIAS FOR $2; rx ALIAS FOR $3; descr ALIAS FOR $4; output TEXT; BEGIN output := ok( result, descr ); RETURN output || CASE result WHEN TRUE THEN '' ELSE E'\n' || diag( ' ' || COALESCE( quote_literal(got), 'NULL' ) || E'\n matches: ' || COALESCE( quote_literal(rx), 'NULL' ) ) END; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION doesnt_match ( anyelement, text, text ) RETURNS TEXT AS $$ SELECT _unalike( $1 !~ $2, $1, $2, $3 ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION doesnt_match ( anyelement, text ) RETURNS TEXT AS $$ SELECT _unalike( $1 !~ $2, $1, $2, NULL ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION doesnt_imatch ( anyelement, text, text ) RETURNS TEXT AS $$ SELECT _unalike( $1 !~* $2, $1, $2, $3 ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION doesnt_imatch ( anyelement, text ) RETURNS TEXT AS $$ SELECT _unalike( $1 !~* $2, $1, $2, NULL ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION unalike ( anyelement, text, text ) RETURNS TEXT AS $$ SELECT _unalike( $1 !~~ $2, $1, $2, $3 ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION unalike ( anyelement, text ) RETURNS TEXT AS $$ SELECT _unalike( $1 !~~ $2, $1, $2, NULL ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION unialike ( anyelement, text, text ) RETURNS TEXT AS $$ SELECT _unalike( $1 !~~* $2, $1, $2, $3 ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION unialike ( anyelement, text ) RETURNS TEXT AS $$ SELECT _unalike( $1 !~~* $2, $1, $2, NULL ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION cmp_ok (anyelement, text, anyelement, text) RETURNS TEXT AS $$ DECLARE have ALIAS FOR $1; op ALIAS FOR $2; want ALIAS FOR $3; descr ALIAS FOR $4; result BOOLEAN; output TEXT; BEGIN EXECUTE 'SELECT ' || COALESCE(quote_literal( have ), 'NULL') || '::' || pg_typeof(have) || ' ' || op || ' ' || COALESCE(quote_literal( want ), 'NULL') || '::' || pg_typeof(want) INTO result; output := ok( COALESCE(result, FALSE), descr ); RETURN output || CASE result WHEN TRUE THEN '' ELSE E'\n' || diag( ' ' || COALESCE( quote_literal(have), 'NULL' ) || E'\n ' || op || E'\n ' || COALESCE( quote_literal(want), 'NULL' ) ) END; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION cmp_ok (anyelement, text, anyelement) RETURNS TEXT AS $$ SELECT cmp_ok( $1, $2, $3, NULL ); $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION pass ( text ) RETURNS TEXT AS $$ SELECT ok( TRUE, $1 ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION pass () RETURNS TEXT AS $$ SELECT ok( TRUE, NULL ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION fail ( text ) RETURNS TEXT AS $$ SELECT ok( FALSE, $1 ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION fail () RETURNS TEXT AS $$ SELECT ok( FALSE, NULL ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION todo ( why text, how_many int ) RETURNS SETOF BOOLEAN AS $$ BEGIN PERFORM _add('todo', COALESCE(how_many, 1), COALESCE(why, '')); RETURN; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION todo ( how_many int, why text ) RETURNS SETOF BOOLEAN AS $$ BEGIN PERFORM _add('todo', COALESCE(how_many, 1), COALESCE(why, '')); RETURN; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION todo ( why text ) RETURNS SETOF BOOLEAN AS $$ BEGIN PERFORM _add('todo', 1, COALESCE(why, '')); RETURN; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION todo ( how_many int ) RETURNS SETOF BOOLEAN AS $$ BEGIN PERFORM _add('todo', COALESCE(how_many, 1), ''); RETURN; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION todo_start (text) RETURNS SETOF BOOLEAN AS $$ BEGIN PERFORM _add('todo', -1, COALESCE($1, '')); RETURN; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION todo_start () RETURNS SETOF BOOLEAN AS $$ BEGIN PERFORM _add('todo', -1, ''); RETURN; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION in_todo () RETURNS BOOLEAN AS $$ DECLARE todos integer; BEGIN todos := _get('todo'); RETURN CASE WHEN todos IS NULL THEN FALSE ELSE TRUE END; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION todo_end () RETURNS SETOF BOOLEAN AS $$ DECLARE id integer; BEGIN id := _get_latest( 'todo', -1 ); IF id IS NULL THEN RAISE EXCEPTION 'todo_end() called without todo_start()'; END IF; EXECUTE 'DELETE FROM __tcache__ WHERE id = ' || id; RETURN; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION _todo() RETURNS TEXT AS $$ DECLARE todos INT[]; note text; BEGIN -- Get the latest id and value, because todo() might have been called -- again before the todos ran out for the first call to todo(). This -- allows them to nest. todos := _get_latest('todo'); IF todos IS NULL THEN -- No todos. RETURN NULL; END IF; IF todos[2] = 0 THEN -- Todos depleted. Clean up. EXECUTE 'DELETE FROM __tcache__ WHERE id = ' || todos[1]; RETURN NULL; END IF; -- Decrement the count of counted todos and return the reason. IF todos[2] <> -1 THEN PERFORM _set(todos[1], todos[2] - 1); END IF; note := _get_note(todos[1]); IF todos[2] = 1 THEN -- This was the last todo, so delete the record. EXECUTE 'DELETE FROM __tcache__ WHERE id = ' || todos[1]; END IF; RETURN note; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION skip ( why text, how_many int ) RETURNS TEXT AS $$ DECLARE output TEXT[]; BEGIN output := '{}'; FOR i IN 1..how_many LOOP output = array_append(output, ok( TRUE, 'SKIP: ' || COALESCE( why, '') ) ); END LOOP; RETURN array_to_string(output, E'\n'); END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION skip ( text ) RETURNS TEXT AS $$ SELECT ok( TRUE, 'SKIP: ' || $1 ); $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION skip( int, text ) RETURNS TEXT AS 'SELECT skip($2, $1)' LANGUAGE sql; CREATE OR REPLACE FUNCTION skip( int ) RETURNS TEXT AS 'SELECT skip(NULL, $1)' LANGUAGE sql; CREATE OR REPLACE FUNCTION _query( TEXT ) RETURNS TEXT AS $$ SELECT CASE WHEN $1 LIKE '"%' OR $1 !~ '[[:space:]]' THEN 'EXECUTE ' || $1 ELSE $1 END; $$ LANGUAGE SQL; -- throws_ok ( sql, errcode, errmsg, description ) CREATE OR REPLACE FUNCTION throws_ok ( TEXT, CHAR(5), TEXT, TEXT ) RETURNS TEXT AS $$ DECLARE query TEXT := _query($1); errcode ALIAS FOR $2; errmsg ALIAS FOR $3; desctext ALIAS FOR $4; descr TEXT; BEGIN descr := COALESCE( desctext, 'threw ' || errcode || ': ' || errmsg, 'threw ' || errcode, 'threw ' || errmsg, 'threw an exception' ); EXECUTE query; RETURN ok( FALSE, descr ) || E'\n' || diag( ' caught: no exception' || E'\n wanted: ' || COALESCE( errcode, 'an exception' ) ); EXCEPTION WHEN OTHERS THEN IF (errcode IS NULL OR SQLSTATE = errcode) AND ( errmsg IS NULL OR SQLERRM = errmsg) THEN -- The expected errcode and/or message was thrown. RETURN ok( TRUE, descr ); ELSE -- This was not the expected errcode or errmsg. RETURN ok( FALSE, descr ) || E'\n' || diag( ' caught: ' || SQLSTATE || ': ' || SQLERRM || E'\n wanted: ' || COALESCE( errcode, 'an exception' ) || COALESCE( ': ' || errmsg, '') ); END IF; END; $$ LANGUAGE plpgsql; -- throws_ok ( sql, errcode, errmsg ) -- throws_ok ( sql, errmsg, description ) CREATE OR REPLACE FUNCTION throws_ok ( TEXT, TEXT, TEXT ) RETURNS TEXT AS $$ BEGIN IF octet_length($2) = 5 THEN RETURN throws_ok( $1, $2::char(5), $3, NULL ); ELSE RETURN throws_ok( $1, NULL, $2, $3 ); END IF; END; $$ LANGUAGE plpgsql; -- throws_ok ( query, errcode ) -- throws_ok ( query, errmsg ) CREATE OR REPLACE FUNCTION throws_ok ( TEXT, TEXT ) RETURNS TEXT AS $$ BEGIN IF octet_length($2) = 5 THEN RETURN throws_ok( $1, $2::char(5), NULL, NULL ); ELSE RETURN throws_ok( $1, NULL, $2, NULL ); END IF; END; $$ LANGUAGE plpgsql; -- throws_ok ( sql ) CREATE OR REPLACE FUNCTION throws_ok ( TEXT ) RETURNS TEXT AS $$ SELECT throws_ok( $1, NULL, NULL, NULL ); $$ LANGUAGE SQL; -- Magically cast integer error codes. -- throws_ok ( sql, errcode, errmsg, description ) CREATE OR REPLACE FUNCTION throws_ok ( TEXT, int4, TEXT, TEXT ) RETURNS TEXT AS $$ SELECT throws_ok( $1, $2::char(5), $3, $4 ); $$ LANGUAGE SQL; -- throws_ok ( sql, errcode, errmsg ) CREATE OR REPLACE FUNCTION throws_ok ( TEXT, int4, TEXT ) RETURNS TEXT AS $$ SELECT throws_ok( $1, $2::char(5), $3, NULL ); $$ LANGUAGE SQL; -- throws_ok ( sql, errcode ) CREATE OR REPLACE FUNCTION throws_ok ( TEXT, int4 ) RETURNS TEXT AS $$ SELECT throws_ok( $1, $2::char(5), NULL, NULL ); $$ LANGUAGE SQL; -- lives_ok( sql, description ) CREATE OR REPLACE FUNCTION lives_ok ( TEXT, TEXT ) RETURNS TEXT AS $$ DECLARE code TEXT := _query($1); descr ALIAS FOR $2; BEGIN EXECUTE code; RETURN ok( TRUE, descr ); EXCEPTION WHEN OTHERS THEN -- There should have been no exception. RETURN ok( FALSE, descr ) || E'\n' || diag( ' died: ' || SQLSTATE || ': ' || SQLERRM ); END; $$ LANGUAGE plpgsql; -- lives_ok( sql ) CREATE OR REPLACE FUNCTION lives_ok ( TEXT ) RETURNS TEXT AS $$ SELECT lives_ok( $1, NULL ); $$ LANGUAGE SQL; -- performs_ok ( sql, milliseconds, description ) CREATE OR REPLACE FUNCTION performs_ok ( TEXT, NUMERIC, TEXT ) RETURNS TEXT AS $$ DECLARE query TEXT := _query($1); max_time ALIAS FOR $2; descr ALIAS FOR $3; starts_at TEXT; act_time NUMERIC; BEGIN starts_at := timeofday(); EXECUTE query; act_time := extract( millisecond from timeofday()::timestamptz - starts_at::timestamptz); IF act_time < max_time THEN RETURN ok(TRUE, descr); END IF; RETURN ok( FALSE, descr ) || E'\n' || diag( ' runtime: ' || act_time || ' ms' || E'\n exceeds: ' || max_time || ' ms' ); END; $$ LANGUAGE plpgsql; -- performs_ok ( sql, milliseconds ) CREATE OR REPLACE FUNCTION performs_ok ( TEXT, NUMERIC ) RETURNS TEXT AS $$ SELECT performs_ok( $1, $2, 'Should run in less than ' || $2 || ' ms' ); $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION display_type ( OID, INTEGER ) RETURNS TEXT AS $$ SELECT COALESCE(substring( pg_catalog.format_type($1, $2), '(("(?!")([^"]|"")+"|[^.]+)([(][^)]+[)])?)$' ), '') $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION display_type ( NAME, OID, INTEGER ) RETURNS TEXT AS $$ SELECT CASE WHEN $1 IS NULL THEN '' ELSE quote_ident($1) || '.' END || display_type($2, $3) $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _ident_array_to_string( name[], text ) RETURNS text AS $$ SELECT array_to_string(ARRAY( SELECT quote_ident($1[i]) FROM generate_series(1, array_upper($1, 1)) s(i) ORDER BY i ), $2); $$ LANGUAGE SQL immutable; -- Borrowed from newsysviews. CREATE OR REPLACE VIEW tap_funky AS SELECT p.oid AS oid, n.nspname AS schema, p.proname AS name, array_to_string(p.proargtypes::regtype[], ',') AS args, CASE p.proretset WHEN TRUE THEN 'setof ' ELSE '' END || p.prorettype::regtype AS returns, p.prolang AS langoid, p.proisstrict AS is_strict, -- p.proisagg AS is_agg, p.prosecdef AS is_definer, p.proretset AS returns_set, p.provolatile::char AS volatility, pg_catalog.pg_function_is_visible(p.oid) AS is_visible FROM pg_catalog.pg_proc p JOIN pg_catalog.pg_namespace n ON p.pronamespace = n.oid ; CREATE OR REPLACE FUNCTION _got_func ( NAME, NAME, NAME[] ) RETURNS BOOLEAN AS $$ SELECT EXISTS( SELECT TRUE FROM tap_funky WHERE schema = $1 AND name = $2 AND args = array_to_string($3, ',') ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _got_func ( NAME, NAME ) RETURNS BOOLEAN AS $$ SELECT EXISTS( SELECT TRUE FROM tap_funky WHERE schema = $1 AND name = $2 ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _got_func ( NAME, NAME[] ) RETURNS BOOLEAN AS $$ SELECT EXISTS( SELECT TRUE FROM tap_funky WHERE name = $1 AND args = array_to_string($2, ',') AND is_visible ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _got_func ( NAME ) RETURNS BOOLEAN AS $$ SELECT EXISTS( SELECT TRUE FROM tap_funky WHERE name = $1 AND is_visible); $$ LANGUAGE SQL; -- has_function( schema, function, args[], description ) CREATE OR REPLACE FUNCTION has_function ( NAME, NAME, NAME[], TEXT ) RETURNS TEXT AS $$ SELECT ok( _got_func($1, $2, $3), $4 ); $$ LANGUAGE SQL; -- has_function( schema, function, args[] ) CREATE OR REPLACE FUNCTION has_function( NAME, NAME, NAME[] ) RETURNS TEXT AS $$ SELECT ok( _got_func($1, $2, $3), 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '(' || array_to_string($3, ', ') || ') should exist' ); $$ LANGUAGE sql; -- has_function( schema, function, description ) CREATE OR REPLACE FUNCTION has_function ( NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( _got_func($1, $2), $3 ); $$ LANGUAGE SQL; -- has_function( schema, function ) CREATE OR REPLACE FUNCTION has_function( NAME, NAME ) RETURNS TEXT AS $$ SELECT ok( _got_func($1, $2), 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '() should exist' ); $$ LANGUAGE sql; -- has_function( function, args[], description ) CREATE OR REPLACE FUNCTION has_function ( NAME, NAME[], TEXT ) RETURNS TEXT AS $$ SELECT ok( _got_func($1, $2), $3 ); $$ LANGUAGE SQL; -- has_function( function, args[] ) CREATE OR REPLACE FUNCTION has_function( NAME, NAME[] ) RETURNS TEXT AS $$ SELECT ok( _got_func($1, $2), 'Function ' || quote_ident($1) || '(' || array_to_string($2, ', ') || ') should exist' ); $$ LANGUAGE sql; -- has_function( function, description ) CREATE OR REPLACE FUNCTION has_function( NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( _got_func($1), $2 ); $$ LANGUAGE sql; -- has_function( function ) CREATE OR REPLACE FUNCTION has_function( NAME ) RETURNS TEXT AS $$ SELECT ok( _got_func($1), 'Function ' || quote_ident($1) || '() should exist' ); $$ LANGUAGE sql; -- hasnt_function( schema, function, args[], description ) CREATE OR REPLACE FUNCTION hasnt_function ( NAME, NAME, NAME[], TEXT ) RETURNS TEXT AS $$ SELECT ok( NOT _got_func($1, $2, $3), $4 ); $$ LANGUAGE SQL; -- hasnt_function( schema, function, args[] ) CREATE OR REPLACE FUNCTION hasnt_function( NAME, NAME, NAME[] ) RETURNS TEXT AS $$ SELECT ok( NOT _got_func($1, $2, $3), 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '(' || array_to_string($3, ', ') || ') should not exist' ); $$ LANGUAGE sql; -- hasnt_function( schema, function, description ) CREATE OR REPLACE FUNCTION hasnt_function ( NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( NOT _got_func($1, $2), $3 ); $$ LANGUAGE SQL; -- hasnt_function( schema, function ) CREATE OR REPLACE FUNCTION hasnt_function( NAME, NAME ) RETURNS TEXT AS $$ SELECT ok( NOT _got_func($1, $2), 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '() should not exist' ); $$ LANGUAGE sql; -- hasnt_function( function, args[], description ) CREATE OR REPLACE FUNCTION hasnt_function ( NAME, NAME[], TEXT ) RETURNS TEXT AS $$ SELECT ok( NOT _got_func($1, $2), $3 ); $$ LANGUAGE SQL; -- hasnt_function( function, args[] ) CREATE OR REPLACE FUNCTION hasnt_function( NAME, NAME[] ) RETURNS TEXT AS $$ SELECT ok( NOT _got_func($1, $2), 'Function ' || quote_ident($1) || '(' || array_to_string($2, ', ') || ') should not exist' ); $$ LANGUAGE sql; -- hasnt_function( function, description ) CREATE OR REPLACE FUNCTION hasnt_function( NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( NOT _got_func($1), $2 ); $$ LANGUAGE sql; -- hasnt_function( function ) CREATE OR REPLACE FUNCTION hasnt_function( NAME ) RETURNS TEXT AS $$ SELECT ok( NOT _got_func($1), 'Function ' || quote_ident($1) || '() should not exist' ); $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION _pg_sv_type_array( OID[] ) RETURNS NAME[] AS $$ SELECT ARRAY( SELECT t.typname FROM pg_catalog.pg_type t JOIN generate_series(1, array_upper($1, 1)) s(i) ON t.oid = $1[i] ORDER BY i ) $$ LANGUAGE SQL stable; -- can( schema, functions[], description ) CREATE OR REPLACE FUNCTION can ( NAME, NAME[], TEXT ) RETURNS TEXT AS $$ DECLARE missing text[]; BEGIN SELECT ARRAY( SELECT quote_ident($2[i]) FROM generate_series(1, array_upper($2, 1)) s(i) LEFT JOIN tap_funky ON name = $2[i] AND schema = $1 WHERE oid IS NULL GROUP BY $2[i], s.i ORDER BY MIN(s.i) ) INTO missing; IF missing[1] IS NULL THEN RETURN ok( true, $3 ); END IF; RETURN ok( false, $3 ) || E'\n' || diag( ' ' || quote_ident($1) || '.' || array_to_string( missing, E'() missing\n ' || quote_ident($1) || '.') || '() missing' ); END; $$ LANGUAGE plpgsql; -- can( schema, functions[] ) CREATE OR REPLACE FUNCTION can ( NAME, NAME[] ) RETURNS TEXT AS $$ SELECT can( $1, $2, 'Schema ' || quote_ident($1) || ' can' ); $$ LANGUAGE sql; -- can( functions[], description ) CREATE OR REPLACE FUNCTION can ( NAME[], TEXT ) RETURNS TEXT AS $$ DECLARE missing text[]; BEGIN SELECT ARRAY( SELECT quote_ident($1[i]) FROM generate_series(1, array_upper($1, 1)) s(i) LEFT JOIN pg_catalog.pg_proc p ON $1[i] = p.proname AND pg_catalog.pg_function_is_visible(p.oid) WHERE p.oid IS NULL ORDER BY s.i ) INTO missing; IF missing[1] IS NULL THEN RETURN ok( true, $2 ); END IF; RETURN ok( false, $2 ) || E'\n' || diag( ' ' || array_to_string( missing, E'() missing\n ') || '() missing' ); END; $$ LANGUAGE plpgsql; -- can( functions[] ) CREATE OR REPLACE FUNCTION can ( NAME[] ) RETURNS TEXT AS $$ SELECT can( $1, 'Schema ' || _ident_array_to_string(current_schemas(true), ' or ') || ' can' ); $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION _has_type( NAME, NAME, CHAR[] ) RETURNS BOOLEAN AS $$ SELECT EXISTS( SELECT true FROM pg_catalog.pg_type t JOIN pg_catalog.pg_namespace n ON t.typnamespace = n.oid WHERE t.typisdefined AND n.nspname = $1 AND t.typname = $2 AND t.typtype = ANY( COALESCE($3, ARRAY['b', 'c', 'd', 'p', 'e']) ) ); $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION _has_type( NAME, CHAR[] ) RETURNS BOOLEAN AS $$ SELECT EXISTS( SELECT true FROM pg_catalog.pg_type t WHERE t.typisdefined AND pg_catalog.pg_type_is_visible(t.oid) AND t.typname = $1 AND t.typtype = ANY( COALESCE($2, ARRAY['b', 'c', 'd', 'p', 'e']) ) ); $$ LANGUAGE sql; -- has_type( schema, type, description ) CREATE OR REPLACE FUNCTION has_type( NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( _has_type( $1, $2, NULL ), $3 ); $$ LANGUAGE sql; -- has_type( schema, type ) CREATE OR REPLACE FUNCTION has_type( NAME, NAME ) RETURNS TEXT AS $$ SELECT has_type( $1, $2, 'Type ' || quote_ident($1) || '.' || quote_ident($2) || ' should exist' ); $$ LANGUAGE sql; -- has_type( type, description ) CREATE OR REPLACE FUNCTION has_type( NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( _has_type( $1, NULL ), $2 ); $$ LANGUAGE sql; -- has_type( type ) CREATE OR REPLACE FUNCTION has_type( NAME ) RETURNS TEXT AS $$ SELECT ok( _has_type( $1, NULL ), ('Type ' || quote_ident($1) || ' should exist')::text ); $$ LANGUAGE sql; -- hasnt_type( schema, type, description ) CREATE OR REPLACE FUNCTION hasnt_type( NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( NOT _has_type( $1, $2, NULL ), $3 ); $$ LANGUAGE sql; -- hasnt_type( schema, type ) CREATE OR REPLACE FUNCTION hasnt_type( NAME, NAME ) RETURNS TEXT AS $$ SELECT hasnt_type( $1, $2, 'Type ' || quote_ident($1) || '.' || quote_ident($2) || ' should not exist' ); $$ LANGUAGE sql; -- hasnt_type( type, description ) CREATE OR REPLACE FUNCTION hasnt_type( NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( NOT _has_type( $1, NULL ), $2 ); $$ LANGUAGE sql; -- hasnt_type( type ) CREATE OR REPLACE FUNCTION hasnt_type( NAME ) RETURNS TEXT AS $$ SELECT ok( NOT _has_type( $1, NULL ), ('Type ' || quote_ident($1) || ' should not exist')::text ); $$ LANGUAGE sql; -- has_domain( schema, domain, description ) CREATE OR REPLACE FUNCTION has_domain( NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( _has_type( $1, $2, ARRAY['d'] ), $3 ); $$ LANGUAGE sql; -- has_domain( schema, domain ) CREATE OR REPLACE FUNCTION has_domain( NAME, NAME ) RETURNS TEXT AS $$ SELECT has_domain( $1, $2, 'Domain ' || quote_ident($1) || '.' || quote_ident($2) || ' should exist' ); $$ LANGUAGE sql; -- has_domain( domain, description ) CREATE OR REPLACE FUNCTION has_domain( NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( _has_type( $1, ARRAY['d'] ), $2 ); $$ LANGUAGE sql; -- has_domain( domain ) CREATE OR REPLACE FUNCTION has_domain( NAME ) RETURNS TEXT AS $$ SELECT ok( _has_type( $1, ARRAY['d'] ), ('Domain ' || quote_ident($1) || ' should exist')::text ); $$ LANGUAGE sql; -- hasnt_domain( schema, domain, description ) CREATE OR REPLACE FUNCTION hasnt_domain( NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( NOT _has_type( $1, $2, ARRAY['d'] ), $3 ); $$ LANGUAGE sql; -- hasnt_domain( schema, domain ) CREATE OR REPLACE FUNCTION hasnt_domain( NAME, NAME ) RETURNS TEXT AS $$ SELECT hasnt_domain( $1, $2, 'Domain ' || quote_ident($1) || '.' || quote_ident($2) || ' should not exist' ); $$ LANGUAGE sql; -- hasnt_domain( domain, description ) CREATE OR REPLACE FUNCTION hasnt_domain( NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( NOT _has_type( $1, ARRAY['d'] ), $2 ); $$ LANGUAGE sql; -- hasnt_domain( domain ) CREATE OR REPLACE FUNCTION hasnt_domain( NAME ) RETURNS TEXT AS $$ SELECT ok( NOT _has_type( $1, ARRAY['d'] ), ('Domain ' || quote_ident($1) || ' should not exist')::text ); $$ LANGUAGE sql; -- has_enum( schema, enum, description ) CREATE OR REPLACE FUNCTION has_enum( NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( _has_type( $1, $2, ARRAY['e'] ), $3 ); $$ LANGUAGE sql; -- has_enum( schema, enum ) CREATE OR REPLACE FUNCTION has_enum( NAME, NAME ) RETURNS TEXT AS $$ SELECT has_enum( $1, $2, 'Enum ' || quote_ident($1) || '.' || quote_ident($2) || ' should exist' ); $$ LANGUAGE sql; -- has_enum( enum, description ) CREATE OR REPLACE FUNCTION has_enum( NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( _has_type( $1, ARRAY['e'] ), $2 ); $$ LANGUAGE sql; -- has_enum( enum ) CREATE OR REPLACE FUNCTION has_enum( NAME ) RETURNS TEXT AS $$ SELECT ok( _has_type( $1, ARRAY['e'] ), ('Enum ' || quote_ident($1) || ' should exist')::text ); $$ LANGUAGE sql; -- hasnt_enum( schema, enum, description ) CREATE OR REPLACE FUNCTION hasnt_enum( NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( NOT _has_type( $1, $2, ARRAY['e'] ), $3 ); $$ LANGUAGE sql; -- hasnt_enum( schema, enum ) CREATE OR REPLACE FUNCTION hasnt_enum( NAME, NAME ) RETURNS TEXT AS $$ SELECT hasnt_enum( $1, $2, 'Enum ' || quote_ident($1) || '.' || quote_ident($2) || ' should not exist' ); $$ LANGUAGE sql; -- hasnt_enum( enum, description ) CREATE OR REPLACE FUNCTION hasnt_enum( NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( NOT _has_type( $1, ARRAY['e'] ), $2 ); $$ LANGUAGE sql; -- hasnt_enum( enum ) CREATE OR REPLACE FUNCTION hasnt_enum( NAME ) RETURNS TEXT AS $$ SELECT ok( NOT _has_type( $1, ARRAY['e'] ), ('Enum ' || quote_ident($1) || ' should not exist')::text ); $$ LANGUAGE sql; -- enum_has_labels( schema, enum, labels, description ) CREATE OR REPLACE FUNCTION enum_has_labels( NAME, NAME, NAME[], TEXT ) RETURNS TEXT AS $$ SELECT is( ARRAY( SELECT e.enumlabel FROM pg_catalog.pg_type t JOIN pg_catalog.pg_enum e ON t.oid = e.enumtypid JOIN pg_catalog.pg_namespace n ON t.typnamespace = n.oid WHERE t.typisdefined AND n.nspname = $1 AND t.typname = $2 AND t.typtype = 'e' ORDER BY e.oid ), $3, $4 ); $$ LANGUAGE sql; -- enum_has_labels( schema, enum, labels ) CREATE OR REPLACE FUNCTION enum_has_labels( NAME, NAME, NAME[] ) RETURNS TEXT AS $$ SELECT enum_has_labels( $1, $2, $3, 'Enum ' || quote_ident($1) || '.' || quote_ident($2) || ' should have labels (' || array_to_string( $3, ', ' ) || ')' ); $$ LANGUAGE sql; -- enum_has_labels( enum, labels, description ) CREATE OR REPLACE FUNCTION enum_has_labels( NAME, NAME[], TEXT ) RETURNS TEXT AS $$ SELECT is( ARRAY( SELECT e.enumlabel FROM pg_catalog.pg_type t JOIN pg_catalog.pg_enum e ON t.oid = e.enumtypid WHERE t.typisdefined AND pg_catalog.pg_type_is_visible(t.oid) AND t.typname = $1 AND t.typtype = 'e' ORDER BY e.oid ), $2, $3 ); $$ LANGUAGE sql; -- enum_has_labels( enum, labels ) CREATE OR REPLACE FUNCTION enum_has_labels( NAME, NAME[] ) RETURNS TEXT AS $$ SELECT enum_has_labels( $1, $2, 'Enum ' || quote_ident($1) || ' should have labels (' || array_to_string( $2, ', ' ) || ')' ); $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION _cmp_types(oid, name) RETURNS BOOLEAN AS $$ DECLARE dtype TEXT := display_type($1, NULL); BEGIN RETURN dtype = _quote_ident_like($2, dtype); END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION _cast_exists ( NAME, NAME, NAME, NAME ) RETURNS BOOLEAN AS $$ SELECT EXISTS ( SELECT TRUE FROM pg_catalog.pg_cast c JOIN pg_catalog.pg_proc p ON c.castfunc = p.oid JOIN pg_catalog.pg_namespace n ON p.pronamespace = n.oid WHERE _cmp_types(castsource, $1) AND _cmp_types(casttarget, $2) AND n.nspname = $3 AND p.proname = $4 ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _cast_exists ( NAME, NAME, NAME ) RETURNS BOOLEAN AS $$ SELECT EXISTS ( SELECT TRUE FROM pg_catalog.pg_cast c JOIN pg_catalog.pg_proc p ON c.castfunc = p.oid WHERE _cmp_types(castsource, $1) AND _cmp_types(casttarget, $2) AND p.proname = $3 ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _cast_exists ( NAME, NAME ) RETURNS BOOLEAN AS $$ SELECT EXISTS ( SELECT TRUE FROM pg_catalog.pg_cast c WHERE _cmp_types(castsource, $1) AND _cmp_types(casttarget, $2) ); $$ LANGUAGE SQL; -- has_cast( source_type, target_type, schema, function, description ) CREATE OR REPLACE FUNCTION has_cast ( NAME, NAME, NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( _cast_exists( $1, $2, $3, $4 ), $5 ); $$ LANGUAGE SQL; -- has_cast( source_type, target_type, schema, function ) CREATE OR REPLACE FUNCTION has_cast ( NAME, NAME, NAME, NAME ) RETURNS TEXT AS $$ SELECT ok( _cast_exists( $1, $2, $3, $4 ), 'Cast (' || quote_ident($1) || ' AS ' || quote_ident($2) || ') WITH FUNCTION ' || quote_ident($3) || '.' || quote_ident($4) || '() should exist' ); $$ LANGUAGE SQL; -- has_cast( source_type, target_type, function, description ) CREATE OR REPLACE FUNCTION has_cast ( NAME, NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( _cast_exists( $1, $2, $3 ), $4 ); $$ LANGUAGE SQL; -- has_cast( source_type, target_type, function ) CREATE OR REPLACE FUNCTION has_cast ( NAME, NAME, NAME ) RETURNS TEXT AS $$ SELECT ok( _cast_exists( $1, $2, $3 ), 'Cast (' || quote_ident($1) || ' AS ' || quote_ident($2) || ') WITH FUNCTION ' || quote_ident($3) || '() should exist' ); $$ LANGUAGE SQL; -- has_cast( source_type, target_type, description ) CREATE OR REPLACE FUNCTION has_cast ( NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( _cast_exists( $1, $2 ), $3 ); $$ LANGUAGE SQL; -- has_cast( source_type, target_type ) CREATE OR REPLACE FUNCTION has_cast ( NAME, NAME ) RETURNS TEXT AS $$ SELECT ok( _cast_exists( $1, $2 ), 'Cast (' || quote_ident($1) || ' AS ' || quote_ident($2) || ') should exist' ); $$ LANGUAGE SQL; -- hasnt_cast( source_type, target_type, schema, function, description ) CREATE OR REPLACE FUNCTION hasnt_cast ( NAME, NAME, NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( NOT _cast_exists( $1, $2, $3, $4 ), $5 ); $$ LANGUAGE SQL; -- hasnt_cast( source_type, target_type, schema, function ) CREATE OR REPLACE FUNCTION hasnt_cast ( NAME, NAME, NAME, NAME ) RETURNS TEXT AS $$ SELECT ok( NOT _cast_exists( $1, $2, $3, $4 ), 'Cast (' || quote_ident($1) || ' AS ' || quote_ident($2) || ') WITH FUNCTION ' || quote_ident($3) || '.' || quote_ident($4) || '() should not exist' ); $$ LANGUAGE SQL; -- hasnt_cast( source_type, target_type, function, description ) CREATE OR REPLACE FUNCTION hasnt_cast ( NAME, NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( NOT _cast_exists( $1, $2, $3 ), $4 ); $$ LANGUAGE SQL; -- hasnt_cast( source_type, target_type, function ) CREATE OR REPLACE FUNCTION hasnt_cast ( NAME, NAME, NAME ) RETURNS TEXT AS $$ SELECT ok( NOT _cast_exists( $1, $2, $3 ), 'Cast (' || quote_ident($1) || ' AS ' || quote_ident($2) || ') WITH FUNCTION ' || quote_ident($3) || '() should not exist' ); $$ LANGUAGE SQL; -- hasnt_cast( source_type, target_type, description ) CREATE OR REPLACE FUNCTION hasnt_cast ( NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( NOT _cast_exists( $1, $2 ), $3 ); $$ LANGUAGE SQL; -- hasnt_cast( source_type, target_type ) CREATE OR REPLACE FUNCTION hasnt_cast ( NAME, NAME ) RETURNS TEXT AS $$ SELECT ok( NOT _cast_exists( $1, $2 ), 'Cast (' || quote_ident($1) || ' AS ' || quote_ident($2) || ') should not exist' ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _expand_context( char ) RETURNS text AS $$ SELECT CASE $1 WHEN 'i' THEN 'implicit' WHEN 'a' THEN 'assignment' WHEN 'e' THEN 'explicit' ELSE 'unknown' END $$ LANGUAGE SQL IMMUTABLE; CREATE OR REPLACE FUNCTION _get_context( NAME, NAME ) RETURNS "char" AS $$ SELECT c.castcontext FROM pg_catalog.pg_cast c WHERE _cmp_types(castsource, $1) AND _cmp_types(casttarget, $2) $$ LANGUAGE SQL; -- cast_context_is( source_type, target_type, context, description ) CREATE OR REPLACE FUNCTION cast_context_is( NAME, NAME, TEXT, TEXT ) RETURNS TEXT AS $$ DECLARE want char = substring(LOWER($3) FROM 1 FOR 1); have char := _get_context($1, $2); BEGIN IF have IS NOT NULL THEN RETURN is( _expand_context(have), _expand_context(want), $4 ); END IF; RETURN ok( false, $4 ) || E'\n' || diag( ' Cast (' || quote_ident($1) || ' AS ' || quote_ident($2) || ') does not exist' ); END; $$ LANGUAGE plpgsql; -- cast_context_is( source_type, target_type, context ) CREATE OR REPLACE FUNCTION cast_context_is( NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT cast_context_is( $1, $2, $3, 'Cast (' || quote_ident($1) || ' AS ' || quote_ident($2) || ') context should be ' || _expand_context(substring(LOWER($3) FROM 1 FOR 1)) ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _op_exists ( NAME, NAME, NAME, NAME, NAME ) RETURNS BOOLEAN AS $$ SELECT EXISTS ( SELECT TRUE FROM pg_catalog.pg_operator o JOIN pg_catalog.pg_namespace n ON o.oprnamespace = n.oid WHERE n.nspname = $2 AND o.oprname = $3 AND CASE o.oprkind WHEN 'l' THEN $1 IS NULL ELSE _cmp_types(o.oprleft, $1) END AND CASE o.oprkind WHEN 'r' THEN $4 IS NULL ELSE _cmp_types(o.oprright, $4) END AND _cmp_types(o.oprresult, $5) ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _op_exists ( NAME, NAME, NAME, NAME ) RETURNS BOOLEAN AS $$ SELECT EXISTS ( SELECT TRUE FROM pg_catalog.pg_operator o WHERE pg_catalog.pg_operator_is_visible(o.oid) AND o.oprname = $2 AND CASE o.oprkind WHEN 'l' THEN $1 IS NULL ELSE _cmp_types(o.oprleft, $1) END AND CASE o.oprkind WHEN 'r' THEN $3 IS NULL ELSE _cmp_types(o.oprright, $3) END AND _cmp_types(o.oprresult, $4) ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _op_exists ( NAME, NAME, NAME ) RETURNS BOOLEAN AS $$ SELECT EXISTS ( SELECT TRUE FROM pg_catalog.pg_operator o WHERE pg_catalog.pg_operator_is_visible(o.oid) AND o.oprname = $2 AND CASE o.oprkind WHEN 'l' THEN $1 IS NULL ELSE _cmp_types(o.oprleft, $1) END AND CASE o.oprkind WHEN 'r' THEN $3 IS NULL ELSE _cmp_types(o.oprright, $3) END ); $$ LANGUAGE SQL; -- has_operator( left_type, schema, name, right_type, return_type, description ) CREATE OR REPLACE FUNCTION has_operator ( NAME, NAME, NAME, NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( _op_exists($1, $2, $3, $4, $5 ), $6 ); $$ LANGUAGE SQL; -- has_operator( left_type, schema, name, right_type, return_type ) CREATE OR REPLACE FUNCTION has_operator ( NAME, NAME, NAME, NAME, NAME ) RETURNS TEXT AS $$ SELECT ok( _op_exists($1, $2, $3, $4, $5 ), 'Operator ' || quote_ident($2) || '.' || $3 || '(' || $1 || ',' || $4 || ') RETURNS ' || $5 || ' should exist' ); $$ LANGUAGE SQL; -- has_operator( left_type, name, right_type, return_type, description ) CREATE OR REPLACE FUNCTION has_operator ( NAME, NAME, NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( _op_exists($1, $2, $3, $4 ), $5 ); $$ LANGUAGE SQL; -- has_operator( left_type, name, right_type, return_type ) CREATE OR REPLACE FUNCTION has_operator ( NAME, NAME, NAME, NAME ) RETURNS TEXT AS $$ SELECT ok( _op_exists($1, $2, $3, $4 ), 'Operator ' || $2 || '(' || $1 || ',' || $3 || ') RETURNS ' || $4 || ' should exist' ); $$ LANGUAGE SQL; -- has_operator( left_type, name, right_type, description ) CREATE OR REPLACE FUNCTION has_operator ( NAME, NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( _op_exists($1, $2, $3 ), $4 ); $$ LANGUAGE SQL; -- has_operator( left_type, name, right_type ) CREATE OR REPLACE FUNCTION has_operator ( NAME, NAME, NAME ) RETURNS TEXT AS $$ SELECT ok( _op_exists($1, $2, $3 ), 'Operator ' || $2 || '(' || $1 || ',' || $3 || ') should exist' ); $$ LANGUAGE SQL; -- has_leftop( schema, name, right_type, return_type, description ) CREATE OR REPLACE FUNCTION has_leftop ( NAME, NAME, NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( _op_exists(NULL, $1, $2, $3, $4), $5 ); $$ LANGUAGE SQL; -- has_leftop( schema, name, right_type, return_type ) CREATE OR REPLACE FUNCTION has_leftop ( NAME, NAME, NAME, NAME ) RETURNS TEXT AS $$ SELECT ok( _op_exists(NULL, $1, $2, $3, $4 ), 'Left operator ' || quote_ident($1) || '.' || $2 || '(NONE,' || $3 || ') RETURNS ' || $4 || ' should exist' ); $$ LANGUAGE SQL; -- has_leftop( name, right_type, return_type, description ) CREATE OR REPLACE FUNCTION has_leftop ( NAME, NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( _op_exists(NULL, $1, $2, $3), $4 ); $$ LANGUAGE SQL; -- has_leftop( name, right_type, return_type ) CREATE OR REPLACE FUNCTION has_leftop ( NAME, NAME, NAME ) RETURNS TEXT AS $$ SELECT ok( _op_exists(NULL, $1, $2, $3 ), 'Left operator ' || $1 || '(NONE,' || $2 || ') RETURNS ' || $3 || ' should exist' ); $$ LANGUAGE SQL; -- has_leftop( name, right_type, description ) CREATE OR REPLACE FUNCTION has_leftop ( NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( _op_exists(NULL, $1, $2), $3 ); $$ LANGUAGE SQL; -- has_leftop( name, right_type ) CREATE OR REPLACE FUNCTION has_leftop ( NAME, NAME ) RETURNS TEXT AS $$ SELECT ok( _op_exists(NULL, $1, $2 ), 'Left operator ' || $1 || '(NONE,' || $2 || ') should exist' ); $$ LANGUAGE SQL; -- has_rightop( left_type, schema, name, return_type, description ) CREATE OR REPLACE FUNCTION has_rightop ( NAME, NAME, NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( _op_exists( $1, $2, $3, NULL, $4), $5 ); $$ LANGUAGE SQL; -- has_rightop( left_type, schema, name, return_type ) CREATE OR REPLACE FUNCTION has_rightop ( NAME, NAME, NAME, NAME ) RETURNS TEXT AS $$ SELECT ok( _op_exists($1, $2, $3, NULL, $4 ), 'Right operator ' || quote_ident($2) || '.' || $3 || '(' || $1 || ',NONE) RETURNS ' || $4 || ' should exist' ); $$ LANGUAGE SQL; -- has_rightop( left_type, name, return_type, description ) CREATE OR REPLACE FUNCTION has_rightop ( NAME, NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( _op_exists( $1, $2, NULL, $3), $4 ); $$ LANGUAGE SQL; -- has_rightop( left_type, name, return_type ) CREATE OR REPLACE FUNCTION has_rightop ( NAME, NAME, NAME ) RETURNS TEXT AS $$ SELECT ok( _op_exists($1, $2, NULL, $3 ), 'Right operator ' || $2 || '(' || $1 || ',NONE) RETURNS ' || $3 || ' should exist' ); $$ LANGUAGE SQL; -- has_rightop( left_type, name, description ) CREATE OR REPLACE FUNCTION has_rightop ( NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( _op_exists( $1, $2, NULL), $3 ); $$ LANGUAGE SQL; -- has_rightop( left_type, name ) CREATE OR REPLACE FUNCTION has_rightop ( NAME, NAME ) RETURNS TEXT AS $$ SELECT ok( _op_exists($1, $2, NULL ), 'Right operator ' || $2 || '(' || $1 || ',NONE) should exist' ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _is_trusted( NAME ) RETURNS BOOLEAN AS $$ SELECT lanpltrusted FROM pg_catalog.pg_language WHERE lanname = $1; $$ LANGUAGE SQL; -- has_language( language, description) CREATE OR REPLACE FUNCTION has_language( NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( _is_trusted($1) IS NOT NULL, $2 ); $$ LANGUAGE SQL; -- has_language( language ) CREATE OR REPLACE FUNCTION has_language( NAME ) RETURNS TEXT AS $$ SELECT ok( _is_trusted($1) IS NOT NULL, 'Procedural language ' || quote_ident($1) || ' should exist' ); $$ LANGUAGE SQL; -- hasnt_language( language, description) CREATE OR REPLACE FUNCTION hasnt_language( NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( _is_trusted($1) IS NULL, $2 ); $$ LANGUAGE SQL; -- hasnt_language( language ) CREATE OR REPLACE FUNCTION hasnt_language( NAME ) RETURNS TEXT AS $$ SELECT ok( _is_trusted($1) IS NULL, 'Procedural language ' || quote_ident($1) || ' should not exist' ); $$ LANGUAGE SQL; -- language_is_trusted( language, description ) CREATE OR REPLACE FUNCTION language_is_trusted( NAME, TEXT ) RETURNS TEXT AS $$ DECLARE is_trusted boolean := _is_trusted($1); BEGIN IF is_trusted IS NULL THEN RETURN fail( $2 ) || E'\n' || diag( ' Procedural language ' || quote_ident($1) || ' does not exist') ; END IF; RETURN ok( is_trusted, $2 ); END; $$ LANGUAGE plpgsql; -- language_is_trusted( language ) CREATE OR REPLACE FUNCTION language_is_trusted( NAME ) RETURNS TEXT AS $$ SELECT language_is_trusted($1, 'Procedural language ' || quote_ident($1) || ' should be trusted' ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _opc_exists( NAME, NAME ) RETURNS BOOLEAN AS $$ SELECT EXISTS ( SELECT TRUE FROM pg_catalog.pg_opclass oc JOIN pg_catalog.pg_namespace n ON oc.opcnamespace = n.oid WHERE n.nspname = COALESCE($1, n.nspname) AND oc.opcname = $2 ); $$ LANGUAGE SQL; -- has_opclass( schema, name, description ) CREATE OR REPLACE FUNCTION has_opclass( NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( _opc_exists( $1, $2 ), $3 ); $$ LANGUAGE SQL; -- has_opclass( schema, name ) CREATE OR REPLACE FUNCTION has_opclass( NAME, NAME ) RETURNS TEXT AS $$ SELECT ok( _opc_exists( $1, $2 ), 'Operator class ' || quote_ident($1) || '.' || quote_ident($2) || ' should exist' ); $$ LANGUAGE SQL; -- has_opclass( name, description ) CREATE OR REPLACE FUNCTION has_opclass( NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( _opc_exists( NULL, $1 ), $2) $$ LANGUAGE SQL; -- has_opclass( name ) CREATE OR REPLACE FUNCTION has_opclass( NAME ) RETURNS TEXT AS $$ SELECT ok( _opc_exists( NULL, $1 ), 'Operator class ' || quote_ident($1) || ' should exist' ); $$ LANGUAGE SQL; -- hasnt_opclass( schema, name, description ) CREATE OR REPLACE FUNCTION hasnt_opclass( NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( NOT _opc_exists( $1, $2 ), $3 ); $$ LANGUAGE SQL; -- hasnt_opclass( schema, name ) CREATE OR REPLACE FUNCTION hasnt_opclass( NAME, NAME ) RETURNS TEXT AS $$ SELECT ok( NOT _opc_exists( $1, $2 ), 'Operator class ' || quote_ident($1) || '.' || quote_ident($2) || ' should exist' ); $$ LANGUAGE SQL; -- hasnt_opclass( name, description ) CREATE OR REPLACE FUNCTION hasnt_opclass( NAME, TEXT ) RETURNS TEXT AS $$ SELECT ok( NOT _opc_exists( NULL, $1 ), $2) $$ LANGUAGE SQL; -- hasnt_opclass( name ) CREATE OR REPLACE FUNCTION hasnt_opclass( NAME ) RETURNS TEXT AS $$ SELECT ok( NOT _opc_exists( NULL, $1 ), 'Operator class ' || quote_ident($1) || ' should exist' ); $$ LANGUAGE SQL; -- opclasses_are( schema, opclasses[], description ) CREATE OR REPLACE FUNCTION _nosuch( NAME, NAME, NAME[]) RETURNS TEXT AS $$ SELECT E'\n' || diag( ' Function ' || CASE WHEN $1 IS NOT NULL THEN quote_ident($1) || '.' ELSE '' END || quote_ident($2) || '(' || array_to_string($3, ', ') || ') does not exist' ); $$ LANGUAGE SQL IMMUTABLE; CREATE OR REPLACE FUNCTION _func_compare( NAME, NAME, NAME[], anyelement, anyelement, TEXT) RETURNS TEXT AS $$ SELECT CASE WHEN $4 IS NULL THEN ok( FALSE, $6 ) || _nosuch($1, $2, $3) ELSE is( $4, $5, $6 ) END; $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _func_compare( NAME, NAME, NAME[], boolean, TEXT) RETURNS TEXT AS $$ SELECT CASE WHEN $4 IS NULL THEN ok( FALSE, $5 ) || _nosuch($1, $2, $3) ELSE ok( $4, $5 ) END; $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION _func_compare( NAME, NAME, anyelement, anyelement, TEXT) RETURNS TEXT AS $$ SELECT CASE WHEN $3 IS NULL THEN ok( FALSE, $5 ) || _nosuch($1, $2, '{}') ELSE is( $3, $4, $5 ) END; $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _func_compare( NAME, NAME, boolean, TEXT) RETURNS TEXT AS $$ SELECT CASE WHEN $3 IS NULL THEN ok( FALSE, $4 ) || _nosuch($1, $2, '{}') ELSE ok( $3, $4 ) END; $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _lang ( NAME, NAME, NAME[] ) RETURNS NAME AS $$ SELECT l.lanname FROM tap_funky f JOIN pg_catalog.pg_language l ON f.langoid = l.oid WHERE f.schema = $1 and f.name = $2 AND f.args = array_to_string($3, ',') $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _lang ( NAME, NAME ) RETURNS NAME AS $$ SELECT l.lanname FROM tap_funky f JOIN pg_catalog.pg_language l ON f.langoid = l.oid WHERE f.schema = $1 and f.name = $2 $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _lang ( NAME, NAME[] ) RETURNS NAME AS $$ SELECT l.lanname FROM tap_funky f JOIN pg_catalog.pg_language l ON f.langoid = l.oid WHERE f.name = $1 AND f.args = array_to_string($2, ',') AND f.is_visible; $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _lang ( NAME ) RETURNS NAME AS $$ SELECT l.lanname FROM tap_funky f JOIN pg_catalog.pg_language l ON f.langoid = l.oid WHERE f.name = $1 AND f.is_visible; $$ LANGUAGE SQL; -- function_lang_is( schema, function, args[], language, description ) CREATE OR REPLACE FUNCTION function_lang_is( NAME, NAME, NAME[], NAME, TEXT ) RETURNS TEXT AS $$ SELECT _func_compare($1, $2, $3, _lang($1, $2, $3), $4, $5 ); $$ LANGUAGE SQL; -- function_lang_is( schema, function, args[], language ) CREATE OR REPLACE FUNCTION function_lang_is( NAME, NAME, NAME[], NAME ) RETURNS TEXT AS $$ SELECT function_lang_is( $1, $2, $3, $4, 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '(' || array_to_string($3, ', ') || ') should be written in ' || quote_ident($4) ); $$ LANGUAGE SQL; -- function_lang_is( schema, function, language, description ) CREATE OR REPLACE FUNCTION function_lang_is( NAME, NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT _func_compare($1, $2, _lang($1, $2), $3, $4 ); $$ LANGUAGE SQL; -- function_lang_is( schema, function, language ) CREATE OR REPLACE FUNCTION function_lang_is( NAME, NAME, NAME ) RETURNS TEXT AS $$ SELECT function_lang_is( $1, $2, $3, 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '() should be written in ' || quote_ident($3) ); $$ LANGUAGE SQL; -- function_lang_is( function, args[], language, description ) CREATE OR REPLACE FUNCTION function_lang_is( NAME, NAME[], NAME, TEXT ) RETURNS TEXT AS $$ SELECT _func_compare(NULL, $1, $2, _lang($1, $2), $3, $4 ); $$ LANGUAGE SQL; -- function_lang_is( function, args[], language ) CREATE OR REPLACE FUNCTION function_lang_is( NAME, NAME[], NAME ) RETURNS TEXT AS $$ SELECT function_lang_is( $1, $2, $3, 'Function ' || quote_ident($1) || '(' || array_to_string($2, ', ') || ') should be written in ' || quote_ident($3) ); $$ LANGUAGE SQL; -- function_lang_is( function, language, description ) CREATE OR REPLACE FUNCTION function_lang_is( NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT _func_compare(NULL, $1, _lang($1), $2, $3 ); $$ LANGUAGE SQL; -- function_lang_is( function, language ) CREATE OR REPLACE FUNCTION function_lang_is( NAME, NAME ) RETURNS TEXT AS $$ SELECT function_lang_is( $1, $2, 'Function ' || quote_ident($1) || '() should be written in ' || quote_ident($2) ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _returns ( NAME, NAME, NAME[] ) RETURNS TEXT AS $$ SELECT returns FROM tap_funky WHERE schema = $1 AND name = $2 AND args = array_to_string($3, ',') $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _returns ( NAME, NAME ) RETURNS TEXT AS $$ SELECT returns FROM tap_funky WHERE schema = $1 AND name = $2 $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _returns ( NAME, NAME[] ) RETURNS TEXT AS $$ SELECT returns FROM tap_funky WHERE name = $1 AND args = array_to_string($2, ',') AND is_visible; $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _returns ( NAME ) RETURNS TEXT AS $$ SELECT returns FROM tap_funky WHERE name = $1 AND is_visible; $$ LANGUAGE SQL; -- function_returns( schema, function, args[], type, description ) CREATE OR REPLACE FUNCTION function_returns( NAME, NAME, NAME[], TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _func_compare($1, $2, $3, _returns($1, $2, $3), $4, $5 ); $$ LANGUAGE SQL; -- function_returns( schema, function, args[], type ) CREATE OR REPLACE FUNCTION function_returns( NAME, NAME, NAME[], TEXT ) RETURNS TEXT AS $$ SELECT function_returns( $1, $2, $3, $4, 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '(' || array_to_string($3, ', ') || ') should return ' || $4 ); $$ LANGUAGE SQL; -- function_returns( schema, function, type, description ) CREATE OR REPLACE FUNCTION function_returns( NAME, NAME, TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _func_compare($1, $2, _returns($1, $2), $3, $4 ); $$ LANGUAGE SQL; -- function_returns( schema, function, type ) CREATE OR REPLACE FUNCTION function_returns( NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT function_returns( $1, $2, $3, 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '() should return ' || $3 ); $$ LANGUAGE SQL; -- function_returns( function, args[], type, description ) CREATE OR REPLACE FUNCTION function_returns( NAME, NAME[], TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _func_compare(NULL, $1, $2, _returns($1, $2), $3, $4 ); $$ LANGUAGE SQL; -- function_returns( function, args[], type ) CREATE OR REPLACE FUNCTION function_returns( NAME, NAME[], TEXT ) RETURNS TEXT AS $$ SELECT function_returns( $1, $2, $3, 'Function ' || quote_ident($1) || '(' || array_to_string($2, ', ') || ') should return ' || $3 ); $$ LANGUAGE SQL; -- function_returns( function, type, description ) CREATE OR REPLACE FUNCTION function_returns( NAME, TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _func_compare(NULL, $1, _returns($1), $2, $3 ); $$ LANGUAGE SQL; -- function_returns( function, type ) CREATE OR REPLACE FUNCTION function_returns( NAME, TEXT ) RETURNS TEXT AS $$ SELECT function_returns( $1, $2, 'Function ' || quote_ident($1) || '() should return ' || $2 ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _definer ( NAME, NAME, NAME[] ) RETURNS BOOLEAN AS $$ SELECT is_definer FROM tap_funky WHERE schema = $1 AND name = $2 AND args = array_to_string($3, ',') $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _definer ( NAME, NAME ) RETURNS BOOLEAN AS $$ SELECT is_definer FROM tap_funky WHERE schema = $1 AND name = $2 $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _definer ( NAME, NAME[] ) RETURNS BOOLEAN AS $$ SELECT is_definer FROM tap_funky WHERE name = $1 AND args = array_to_string($2, ',') AND is_visible; $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _definer ( NAME ) RETURNS BOOLEAN AS $$ SELECT is_definer FROM tap_funky WHERE name = $1 AND is_visible; $$ LANGUAGE SQL; -- is_definer( schema, function, args[], description ) CREATE OR REPLACE FUNCTION is_definer ( NAME, NAME, NAME[], TEXT ) RETURNS TEXT AS $$ SELECT _func_compare($1, $2, $3, _definer($1, $2, $3), $4 ); $$ LANGUAGE SQL; -- is_definer( schema, function, args[] ) CREATE OR REPLACE FUNCTION is_definer( NAME, NAME, NAME[] ) RETURNS TEXT AS $$ SELECT ok( _definer($1, $2, $3), 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '(' || array_to_string($3, ', ') || ') should be security definer' ); $$ LANGUAGE sql; -- is_definer( schema, function, description ) CREATE OR REPLACE FUNCTION is_definer ( NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT _func_compare($1, $2, _definer($1, $2), $3 ); $$ LANGUAGE SQL; -- is_definer( schema, function ) CREATE OR REPLACE FUNCTION is_definer( NAME, NAME ) RETURNS TEXT AS $$ SELECT ok( _definer($1, $2), 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '() should be security definer' ); $$ LANGUAGE sql; -- is_definer( function, args[], description ) CREATE OR REPLACE FUNCTION is_definer ( NAME, NAME[], TEXT ) RETURNS TEXT AS $$ SELECT _func_compare(NULL, $1, $2, _definer($1, $2), $3 ); $$ LANGUAGE SQL; -- is_definer( function, args[] ) CREATE OR REPLACE FUNCTION is_definer( NAME, NAME[] ) RETURNS TEXT AS $$ SELECT ok( _definer($1, $2), 'Function ' || quote_ident($1) || '(' || array_to_string($2, ', ') || ') should be security definer' ); $$ LANGUAGE sql; -- is_definer( function, description ) CREATE OR REPLACE FUNCTION is_definer( NAME, TEXT ) RETURNS TEXT AS $$ SELECT _func_compare(NULL, $1, _definer($1), $2 ); $$ LANGUAGE sql; -- is_definer( function ) CREATE OR REPLACE FUNCTION is_definer( NAME ) RETURNS TEXT AS $$ SELECT ok( _definer($1), 'Function ' || quote_ident($1) || '() should be security definer' ); $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION _strict ( NAME, NAME, NAME[] ) RETURNS BOOLEAN AS $$ SELECT is_strict FROM tap_funky WHERE schema = $1 AND name = $2 AND args = array_to_string($3, ',') $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _strict ( NAME, NAME ) RETURNS BOOLEAN AS $$ SELECT is_strict FROM tap_funky WHERE schema = $1 AND name = $2 $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _strict ( NAME, NAME[] ) RETURNS BOOLEAN AS $$ SELECT is_strict FROM tap_funky WHERE name = $1 AND args = array_to_string($2, ',') AND is_visible; $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _strict ( NAME ) RETURNS BOOLEAN AS $$ SELECT is_strict FROM tap_funky WHERE name = $1 AND is_visible; $$ LANGUAGE SQL; -- is_strict( schema, function, args[], description ) CREATE OR REPLACE FUNCTION is_strict ( NAME, NAME, NAME[], TEXT ) RETURNS TEXT AS $$ SELECT _func_compare($1, $2, $3, _strict($1, $2, $3), $4 ); $$ LANGUAGE SQL; -- is_strict( schema, function, args[] ) CREATE OR REPLACE FUNCTION is_strict( NAME, NAME, NAME[] ) RETURNS TEXT AS $$ SELECT ok( _strict($1, $2, $3), 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '(' || array_to_string($3, ', ') || ') should be strict' ); $$ LANGUAGE sql; -- is_strict( schema, function, description ) CREATE OR REPLACE FUNCTION is_strict ( NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT _func_compare($1, $2, _strict($1, $2), $3 ); $$ LANGUAGE SQL; -- is_strict( schema, function ) CREATE OR REPLACE FUNCTION is_strict( NAME, NAME ) RETURNS TEXT AS $$ SELECT ok( _strict($1, $2), 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '() should be strict' ); $$ LANGUAGE sql; -- is_strict( function, args[], description ) CREATE OR REPLACE FUNCTION is_strict ( NAME, NAME[], TEXT ) RETURNS TEXT AS $$ SELECT _func_compare(NULL, $1, $2, _strict($1, $2), $3 ); $$ LANGUAGE SQL; -- is_strict( function, args[] ) CREATE OR REPLACE FUNCTION is_strict( NAME, NAME[] ) RETURNS TEXT AS $$ SELECT ok( _strict($1, $2), 'Function ' || quote_ident($1) || '(' || array_to_string($2, ', ') || ') should be strict' ); $$ LANGUAGE sql; -- is_strict( function, description ) CREATE OR REPLACE FUNCTION is_strict( NAME, TEXT ) RETURNS TEXT AS $$ SELECT _func_compare(NULL, $1, _strict($1), $2 ); $$ LANGUAGE sql; -- is_strict( function ) CREATE OR REPLACE FUNCTION is_strict( NAME ) RETURNS TEXT AS $$ SELECT ok( _strict($1), 'Function ' || quote_ident($1) || '() should be strict' ); $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION _expand_vol( char ) RETURNS TEXT AS $$ SELECT CASE $1 WHEN 'i' THEN 'IMMUTABLE' WHEN 's' THEN 'STABLE' WHEN 'v' THEN 'VOLATILE' ELSE 'UNKNOWN' END $$ LANGUAGE SQL IMMUTABLE; CREATE OR REPLACE FUNCTION _refine_vol( text ) RETURNS text AS $$ SELECT _expand_vol(substring(LOWER($1) FROM 1 FOR 1)::char); $$ LANGUAGE SQL IMMUTABLE; CREATE OR REPLACE FUNCTION _vol ( NAME, NAME, NAME[] ) RETURNS TEXT AS $$ SELECT _expand_vol(volatility) FROM tap_funky f WHERE f.schema = $1 and f.name = $2 AND f.args = array_to_string($3, ',') $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _vol ( NAME, NAME ) RETURNS TEXT AS $$ SELECT _expand_vol(volatility) FROM tap_funky f WHERE f.schema = $1 and f.name = $2 $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _vol ( NAME, NAME[] ) RETURNS TEXT AS $$ SELECT _expand_vol(volatility) FROM tap_funky f WHERE f.name = $1 AND f.args = array_to_string($2, ',') AND f.is_visible; $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _vol ( NAME ) RETURNS TEXT AS $$ SELECT _expand_vol(volatility) FROM tap_funky f WHERE f.name = $1 AND f.is_visible; $$ LANGUAGE SQL; -- volatility_is( schema, function, args[], volatility, description ) CREATE OR REPLACE FUNCTION volatility_is( NAME, NAME, NAME[], TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _func_compare($1, $2, $3, _vol($1, $2, $3), _refine_vol($4), $5 ); $$ LANGUAGE SQL; -- volatility_is( schema, function, args[], volatility ) CREATE OR REPLACE FUNCTION volatility_is( NAME, NAME, NAME[], TEXT ) RETURNS TEXT AS $$ SELECT volatility_is( $1, $2, $3, $4, 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '(' || array_to_string($3, ', ') || ') should be ' || _refine_vol($4) ); $$ LANGUAGE SQL; -- volatility_is( schema, function, volatility, description ) CREATE OR REPLACE FUNCTION volatility_is( NAME, NAME, TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _func_compare($1, $2, _vol($1, $2), _refine_vol($3), $4 ); $$ LANGUAGE SQL; -- volatility_is( schema, function, volatility ) CREATE OR REPLACE FUNCTION volatility_is( NAME, NAME, TEXT ) RETURNS TEXT AS $$ SELECT volatility_is( $1, $2, $3, 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '() should be ' || _refine_vol($3) ); $$ LANGUAGE SQL; -- volatility_is( function, args[], volatility, description ) CREATE OR REPLACE FUNCTION volatility_is( NAME, NAME[], TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _func_compare(NULL, $1, $2, _vol($1, $2), _refine_vol($3), $4 ); $$ LANGUAGE SQL; -- volatility_is( function, args[], volatility ) CREATE OR REPLACE FUNCTION volatility_is( NAME, NAME[], TEXT ) RETURNS TEXT AS $$ SELECT volatility_is( $1, $2, $3, 'Function ' || quote_ident($1) || '(' || array_to_string($2, ', ') || ') should be ' || _refine_vol($3) ); $$ LANGUAGE SQL; -- volatility_is( function, volatility, description ) CREATE OR REPLACE FUNCTION volatility_is( NAME, TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _func_compare(NULL, $1, _vol($1), _refine_vol($2), $3 ); $$ LANGUAGE SQL; -- volatility_is( function, volatility ) CREATE OR REPLACE FUNCTION volatility_is( NAME, TEXT ) RETURNS TEXT AS $$ SELECT volatility_is( $1, $2, 'Function ' || quote_ident($1) || '() should be ' || _refine_vol($2) ); $$ LANGUAGE SQL; -- check_test( test_output, pass, name, description, diag, match_diag ) CREATE OR REPLACE FUNCTION findfuncs( NAME, TEXT ) RETURNS TEXT[] AS $$ SELECT ARRAY( SELECT DISTINCT quote_ident(n.nspname) || '.' || quote_ident(p.proname) AS pname FROM pg_catalog.pg_proc p JOIN pg_catalog.pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = $1 AND p.proname ~ $2 ORDER BY pname ); $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION findfuncs( TEXT ) RETURNS TEXT[] AS $$ SELECT ARRAY( SELECT DISTINCT quote_ident(n.nspname) || '.' || quote_ident(p.proname) AS pname FROM pg_catalog.pg_proc p JOIN pg_catalog.pg_namespace n ON p.pronamespace = n.oid WHERE pg_catalog.pg_function_is_visible(p.oid) AND p.proname ~ $1 ORDER BY pname ); $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION _runem( text[], boolean ) RETURNS SETOF TEXT AS $$ DECLARE tap text; lbound int := array_lower($1, 1); BEGIN IF lbound IS NULL THEN RETURN; END IF; FOR i IN lbound..array_upper($1, 1) LOOP -- Send the name of the function to diag if warranted. IF $2 THEN RETURN NEXT diag( $1[i] || '()' ); END IF; -- Execute the tap function and return its results. FOR tap IN EXECUTE 'SELECT * FROM ' || $1[i] || '()' LOOP RETURN NEXT tap; END LOOP; END LOOP; RETURN; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION _is_verbose() RETURNS BOOLEAN AS $$ SELECT current_setting('client_min_messages') NOT IN ( 'warning', 'error', 'fatal', 'panic' ); $$ LANGUAGE sql STABLE; -- do_tap( schema, pattern ) CREATE OR REPLACE FUNCTION do_tap( name, text ) RETURNS SETOF TEXT AS $$ SELECT * FROM _runem( findfuncs($1, $2), _is_verbose() ); $$ LANGUAGE sql; -- do_tap( schema ) CREATE OR REPLACE FUNCTION do_tap( name ) RETURNS SETOF TEXT AS $$ SELECT * FROM _runem( findfuncs($1, '^test'), _is_verbose() ); $$ LANGUAGE sql; -- do_tap( pattern ) CREATE OR REPLACE FUNCTION do_tap( text ) RETURNS SETOF TEXT AS $$ SELECT * FROM _runem( findfuncs($1), _is_verbose() ); $$ LANGUAGE sql; -- do_tap() CREATE OR REPLACE FUNCTION do_tap( ) RETURNS SETOF TEXT AS $$ SELECT * FROM _runem( findfuncs('^test'), _is_verbose()); $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION _currtest() RETURNS INTEGER AS $$ BEGIN RETURN currval('__tresults___numb_seq'); EXCEPTION WHEN object_not_in_prerequisite_state THEN RETURN 0; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION _cleanup() RETURNS boolean AS $$ DROP TABLE __tresults__; DROP SEQUENCE __tresults___numb_seq; DROP TABLE __tcache__; DROP SEQUENCE __tcache___id_seq; SELECT TRUE; $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION _runner( text[], text[], text[], text[], text[] ) RETURNS SETOF TEXT AS $$ DECLARE startup ALIAS FOR $1; shutdown ALIAS FOR $2; setup ALIAS FOR $3; teardown ALIAS FOR $4; tests ALIAS FOR $5; tap text; verbos boolean := _is_verbose(); -- verbose is a reserved word in 8.5. num_faild INTEGER := 0; BEGIN BEGIN -- No plan support. PERFORM * FROM no_plan(); FOR tap IN SELECT * FROM _runem(startup, false) LOOP RETURN NEXT tap; END LOOP; EXCEPTION -- Catch all exceptions and simply rethrow custom exceptions. This -- will roll back everything in the above block. WHEN raise_exception THEN RAISE EXCEPTION '%', SQLERRM; END; BEGIN FOR i IN 1..array_upper(tests, 1) LOOP BEGIN -- What test are we running? IF verbos THEN RETURN NEXT diag(tests[i] || '()'); END IF; -- Run the setup functions. FOR tap IN SELECT * FROM _runem(setup, false) LOOP RETURN NEXT tap; END LOOP; -- Run the actual test function. FOR tap IN EXECUTE 'SELECT * FROM ' || tests[i] || '()' LOOP RETURN NEXT tap; END LOOP; -- Run the teardown functions. FOR tap IN SELECT * FROM _runem(teardown, false) LOOP RETURN NEXT tap; END LOOP; -- Remember how many failed and then roll back. num_faild := num_faild + num_failed(); RAISE EXCEPTION '__TAP_ROLLBACK__'; EXCEPTION WHEN raise_exception THEN IF SQLERRM <> '__TAP_ROLLBACK__' THEN -- We didn't raise it, so propagate it. RAISE EXCEPTION '%', SQLERRM; END IF; END; END LOOP; -- Run the shutdown functions. FOR tap IN SELECT * FROM _runem(shutdown, false) LOOP RETURN NEXT tap; END LOOP; -- Raise an exception to rollback any changes. RAISE EXCEPTION '__TAP_ROLLBACK__'; EXCEPTION WHEN raise_exception THEN IF SQLERRM <> '__TAP_ROLLBACK__' THEN -- We didn't raise it, so propagate it. RAISE EXCEPTION '%', SQLERRM; END IF; END; -- Finish up. FOR tap IN SELECT * FROM _finish( currval('__tresults___numb_seq')::integer, 0, num_faild ) LOOP RETURN NEXT tap; END LOOP; -- Clean up and return. PERFORM _cleanup(); RETURN; END; $$ LANGUAGE plpgsql; -- runtests( schema, match ) CREATE OR REPLACE FUNCTION runtests( NAME, TEXT ) RETURNS SETOF TEXT AS $$ SELECT * FROM _runner( findfuncs( $1, '^startup' ), findfuncs( $1, '^shutdown' ), findfuncs( $1, '^setup' ), findfuncs( $1, '^teardown' ), findfuncs( $1, $2 ) ); $$ LANGUAGE sql; -- runtests( schema ) CREATE OR REPLACE FUNCTION runtests( NAME ) RETURNS SETOF TEXT AS $$ SELECT * FROM runtests( $1, '^test' ); $$ LANGUAGE sql; -- runtests( match ) CREATE OR REPLACE FUNCTION runtests( TEXT ) RETURNS SETOF TEXT AS $$ SELECT * FROM _runner( findfuncs( '^startup' ), findfuncs( '^shutdown' ), findfuncs( '^setup' ), findfuncs( '^teardown' ), findfuncs( $1 ) ); $$ LANGUAGE sql; -- runtests( ) CREATE OR REPLACE FUNCTION runtests( ) RETURNS SETOF TEXT AS $$ SELECT * FROM runtests( '^test' ); $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION _temptable ( TEXT, TEXT ) RETURNS TEXT AS $$ BEGIN EXECUTE 'CREATE TEMP TABLE ' || $2 || ' AS ' || _query($1); return $2; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION _temptable ( anyarray, TEXT ) RETURNS TEXT AS $$ BEGIN CREATE TEMP TABLE _____coltmp___ AS SELECT $1[i] FROM generate_series(array_lower($1, 1), array_upper($1, 1)) s(i); EXECUTE 'ALTER TABLE _____coltmp___ RENAME TO ' || $2; return $2; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION _temptypes( TEXT ) RETURNS TEXT AS $$ SELECT array_to_string(ARRAY( SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) FROM pg_catalog.pg_attribute a JOIN pg_catalog.pg_class c ON a.attrelid = c.oid JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid WHERE c.relname = $1 AND n.nspname LIKE 'pg_temp%' AND attnum > 0 AND NOT attisdropped ORDER BY attnum ), ','); $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION _docomp( TEXT, TEXT, TEXT, TEXT ) RETURNS TEXT AS $$ DECLARE have ALIAS FOR $1; want ALIAS FOR $2; extras TEXT[] := '{}'; missing TEXT[] := '{}'; res BOOLEAN := TRUE; msg TEXT := ''; rec RECORD; BEGIN BEGIN -- Find extra records. FOR rec in EXECUTE 'SELECT * FROM ' || have || ' EXCEPT ' || $4 || 'SELECT * FROM ' || want LOOP extras := extras || rec::text; END LOOP; -- Find missing records. FOR rec in EXECUTE 'SELECT * FROM ' || want || ' EXCEPT ' || $4 || 'SELECT * FROM ' || have LOOP missing := missing || rec::text; END LOOP; -- Drop the temporary tables. EXECUTE 'DROP TABLE ' || have; EXECUTE 'DROP TABLE ' || want; EXCEPTION WHEN syntax_error OR datatype_mismatch THEN msg := E'\n' || diag( E' Columns differ between queries:\n' || ' have: (' || _temptypes(have) || E')\n' || ' want: (' || _temptypes(want) || ')' ); EXECUTE 'DROP TABLE ' || have; EXECUTE 'DROP TABLE ' || want; RETURN ok(FALSE, $3) || msg; END; -- What extra records do we have? IF extras[1] IS NOT NULL THEN res := FALSE; msg := E'\n' || diag( E' Extra records:\n ' || array_to_string( extras, E'\n ' ) ); END IF; -- What missing records do we have? IF missing[1] IS NOT NULL THEN res := FALSE; msg := msg || E'\n' || diag( E' Missing records:\n ' || array_to_string( missing, E'\n ' ) ); END IF; RETURN ok(res, $3) || msg; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION _relcomp( TEXT, TEXT, TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _docomp( _temptable( $1, '__taphave__' ), _temptable( $2, '__tapwant__' ), $3, $4 ); $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION _relcomp( TEXT, anyarray, TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _docomp( _temptable( $1, '__taphave__' ), _temptable( $2, '__tapwant__' ), $3, $4 ); $$ LANGUAGE sql; -- set_eq( sql, sql, description ) CREATE OR REPLACE FUNCTION set_eq( TEXT, TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _relcomp( $1, $2, $3, '' ); $$ LANGUAGE sql; -- set_eq( sql, sql ) CREATE OR REPLACE FUNCTION set_eq( TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _relcomp( $1, $2, NULL::text, '' ); $$ LANGUAGE sql; -- set_eq( sql, array, description ) CREATE OR REPLACE FUNCTION set_eq( TEXT, anyarray, TEXT ) RETURNS TEXT AS $$ SELECT _relcomp( $1, $2, $3, '' ); $$ LANGUAGE sql; -- set_eq( sql, array ) CREATE OR REPLACE FUNCTION set_eq( TEXT, anyarray ) RETURNS TEXT AS $$ SELECT _relcomp( $1, $2, NULL::text, '' ); $$ LANGUAGE sql; -- bag_eq( sql, sql, description ) CREATE OR REPLACE FUNCTION bag_eq( TEXT, TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _relcomp( $1, $2, $3, 'ALL ' ); $$ LANGUAGE sql; -- bag_eq( sql, sql ) CREATE OR REPLACE FUNCTION bag_eq( TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _relcomp( $1, $2, NULL::text, 'ALL ' ); $$ LANGUAGE sql; -- bag_eq( sql, array, description ) CREATE OR REPLACE FUNCTION bag_eq( TEXT, anyarray, TEXT ) RETURNS TEXT AS $$ SELECT _relcomp( $1, $2, $3, 'ALL ' ); $$ LANGUAGE sql; -- bag_eq( sql, array ) CREATE OR REPLACE FUNCTION bag_eq( TEXT, anyarray ) RETURNS TEXT AS $$ SELECT _relcomp( $1, $2, NULL::text, 'ALL ' ); $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION _do_ne( TEXT, TEXT, TEXT, TEXT ) RETURNS TEXT AS $$ DECLARE have ALIAS FOR $1; want ALIAS FOR $2; extras TEXT[] := '{}'; missing TEXT[] := '{}'; res BOOLEAN := TRUE; msg TEXT := ''; BEGIN BEGIN -- Find extra records. EXECUTE 'SELECT EXISTS ( ' || '( SELECT * FROM ' || have || ' EXCEPT ' || $4 || ' SELECT * FROM ' || want || ' ) UNION ( ' || ' SELECT * FROM ' || want || ' EXCEPT ' || $4 || ' SELECT * FROM ' || have || ' ) LIMIT 1 )' INTO res; -- Drop the temporary tables. EXECUTE 'DROP TABLE ' || have; EXECUTE 'DROP TABLE ' || want; EXCEPTION WHEN syntax_error OR datatype_mismatch THEN msg := E'\n' || diag( E' Columns differ between queries:\n' || ' have: (' || _temptypes(have) || E')\n' || ' want: (' || _temptypes(want) || ')' ); EXECUTE 'DROP TABLE ' || have; EXECUTE 'DROP TABLE ' || want; RETURN ok(FALSE, $3) || msg; END; -- Return the value from the query. RETURN ok(res, $3); END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION _relne( TEXT, TEXT, TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _do_ne( _temptable( $1, '__taphave__' ), _temptable( $2, '__tapwant__' ), $3, $4 ); $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION _relne( TEXT, anyarray, TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _do_ne( _temptable( $1, '__taphave__' ), _temptable( $2, '__tapwant__' ), $3, $4 ); $$ LANGUAGE sql; -- set_ne( sql, sql, description ) CREATE OR REPLACE FUNCTION set_ne( TEXT, TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _relne( $1, $2, $3, '' ); $$ LANGUAGE sql; -- set_ne( sql, sql ) CREATE OR REPLACE FUNCTION set_ne( TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _relne( $1, $2, NULL::text, '' ); $$ LANGUAGE sql; -- set_ne( sql, array, description ) CREATE OR REPLACE FUNCTION set_ne( TEXT, anyarray, TEXT ) RETURNS TEXT AS $$ SELECT _relne( $1, $2, $3, '' ); $$ LANGUAGE sql; -- set_ne( sql, array ) CREATE OR REPLACE FUNCTION set_ne( TEXT, anyarray ) RETURNS TEXT AS $$ SELECT _relne( $1, $2, NULL::text, '' ); $$ LANGUAGE sql; -- bag_ne( sql, sql, description ) CREATE OR REPLACE FUNCTION bag_ne( TEXT, TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _relne( $1, $2, $3, 'ALL ' ); $$ LANGUAGE sql; -- bag_ne( sql, sql ) CREATE OR REPLACE FUNCTION bag_ne( TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _relne( $1, $2, NULL::text, 'ALL ' ); $$ LANGUAGE sql; -- bag_ne( sql, array, description ) CREATE OR REPLACE FUNCTION bag_ne( TEXT, anyarray, TEXT ) RETURNS TEXT AS $$ SELECT _relne( $1, $2, $3, 'ALL ' ); $$ LANGUAGE sql; -- bag_ne( sql, array ) CREATE OR REPLACE FUNCTION bag_ne( TEXT, anyarray ) RETURNS TEXT AS $$ SELECT _relne( $1, $2, NULL::text, 'ALL ' ); $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION _relcomp( TEXT, TEXT, TEXT, TEXT, TEXT ) RETURNS TEXT AS $$ DECLARE have TEXT := _temptable( $1, '__taphave__' ); want TEXT := _temptable( $2, '__tapwant__' ); results TEXT[] := '{}'; res BOOLEAN := TRUE; msg TEXT := ''; rec RECORD; BEGIN BEGIN -- Find relevant records. FOR rec in EXECUTE 'SELECT * FROM ' || want || ' ' || $4 || ' SELECT * FROM ' || have LOOP results := results || rec::text; END LOOP; -- Drop the temporary tables. EXECUTE 'DROP TABLE ' || have; EXECUTE 'DROP TABLE ' || want; EXCEPTION WHEN syntax_error OR datatype_mismatch THEN msg := E'\n' || diag( E' Columns differ between queries:\n' || ' have: (' || _temptypes(have) || E')\n' || ' want: (' || _temptypes(want) || ')' ); EXECUTE 'DROP TABLE ' || have; EXECUTE 'DROP TABLE ' || want; RETURN ok(FALSE, $3) || msg; END; -- What records do we have? IF results[1] IS NOT NULL THEN res := FALSE; msg := msg || E'\n' || diag( ' ' || $5 || E' records:\n ' || array_to_string( results, E'\n ' ) ); END IF; RETURN ok(res, $3) || msg; END; $$ LANGUAGE plpgsql; -- set_has( sql, sql, description ) CREATE OR REPLACE FUNCTION set_has( TEXT, TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _relcomp( $1, $2, $3, 'EXCEPT', 'Missing' ); $$ LANGUAGE sql; -- set_has( sql, sql ) CREATE OR REPLACE FUNCTION set_has( TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _relcomp( $1, $2, NULL::TEXT, 'EXCEPT', 'Missing' ); $$ LANGUAGE sql; -- bag_has( sql, sql, description ) CREATE OR REPLACE FUNCTION bag_has( TEXT, TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _relcomp( $1, $2, $3, 'EXCEPT ALL', 'Missing' ); $$ LANGUAGE sql; -- bag_has( sql, sql ) CREATE OR REPLACE FUNCTION bag_has( TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _relcomp( $1, $2, NULL::TEXT, 'EXCEPT ALL', 'Missing' ); $$ LANGUAGE sql; -- set_hasnt( sql, sql, description ) CREATE OR REPLACE FUNCTION set_hasnt( TEXT, TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _relcomp( $1, $2, $3, 'INTERSECT', 'Extra' ); $$ LANGUAGE sql; -- set_hasnt( sql, sql ) CREATE OR REPLACE FUNCTION set_hasnt( TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _relcomp( $1, $2, NULL::TEXT, 'INTERSECT', 'Extra' ); $$ LANGUAGE sql; -- bag_hasnt( sql, sql, description ) CREATE OR REPLACE FUNCTION bag_hasnt( TEXT, TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _relcomp( $1, $2, $3, 'INTERSECT ALL', 'Extra' ); $$ LANGUAGE sql; -- bag_hasnt( sql, sql ) CREATE OR REPLACE FUNCTION bag_hasnt( TEXT, TEXT ) RETURNS TEXT AS $$ SELECT _relcomp( $1, $2, NULL::TEXT, 'INTERSECT ALL', 'Extra' ); $$ LANGUAGE sql; -- results_eq( cursor, cursor, description ) CREATE OR REPLACE FUNCTION results_eq( refcursor, refcursor, text ) RETURNS TEXT AS $$ DECLARE have ALIAS FOR $1; want ALIAS FOR $2; have_rec RECORD; want_rec RECORD; have_found BOOLEAN; want_found BOOLEAN; rownum INTEGER := 1; BEGIN FETCH have INTO have_rec; have_found := FOUND; FETCH want INTO want_rec; want_found := FOUND; WHILE have_found OR want_found LOOP IF have_rec::text IS DISTINCT FROM want_rec::text OR have_found <> want_found THEN RETURN ok( false, $3 ) || E'\n' || diag( ' Results differ beginning at row ' || rownum || E':\n' || ' have: ' || CASE WHEN have_found THEN have_rec::text ELSE 'NULL' END || E'\n' || ' want: ' || CASE WHEN want_found THEN want_rec::text ELSE 'NULL' END ); END IF; rownum = rownum + 1; FETCH have INTO have_rec; have_found := FOUND; FETCH want INTO want_rec; want_found := FOUND; END LOOP; RETURN ok( true, $3 ); EXCEPTION WHEN datatype_mismatch THEN RETURN ok( false, $3 ) || E'\n' || diag( E' Columns differ between queries:\n' || ' have: ' || CASE WHEN have_found THEN have_rec::text ELSE 'NULL' END || E'\n' || ' want: ' || CASE WHEN want_found THEN want_rec::text ELSE 'NULL' END ); END; $$ LANGUAGE plpgsql; -- results_eq( cursor, cursor ) CREATE OR REPLACE FUNCTION results_eq( refcursor, refcursor ) RETURNS TEXT AS $$ SELECT results_eq( $1, $2, NULL::text ); $$ LANGUAGE sql; -- results_eq( sql, sql, description ) CREATE OR REPLACE FUNCTION results_eq( TEXT, TEXT, TEXT ) RETURNS TEXT AS $$ DECLARE have REFCURSOR; want REFCURSOR; res TEXT; BEGIN OPEN have FOR EXECUTE _query($1); OPEN want FOR EXECUTE _query($2); res := results_eq(have, want, $3); CLOSE have; CLOSE want; RETURN res; END; $$ LANGUAGE plpgsql; -- results_eq( sql, sql ) CREATE OR REPLACE FUNCTION results_eq( TEXT, TEXT ) RETURNS TEXT AS $$ SELECT results_eq( $1, $2, NULL::text ); $$ LANGUAGE sql; -- results_eq( sql, array, description ) CREATE OR REPLACE FUNCTION results_eq( TEXT, anyarray, TEXT ) RETURNS TEXT AS $$ DECLARE have REFCURSOR; want REFCURSOR; res TEXT; BEGIN OPEN have FOR EXECUTE _query($1); OPEN want FOR SELECT $2[i] FROM generate_series(array_lower($2, 1), array_upper($2, 1)) s(i); res := results_eq(have, want, $3); CLOSE have; CLOSE want; RETURN res; END; $$ LANGUAGE plpgsql; -- results_eq( sql, array ) CREATE OR REPLACE FUNCTION results_eq( TEXT, anyarray ) RETURNS TEXT AS $$ SELECT results_eq( $1, $2, NULL::text ); $$ LANGUAGE sql; -- results_eq( sql, cursor, description ) CREATE OR REPLACE FUNCTION results_eq( TEXT, refcursor, TEXT ) RETURNS TEXT AS $$ DECLARE have REFCURSOR; res TEXT; BEGIN OPEN have FOR EXECUTE _query($1); res := results_eq(have, $2, $3); CLOSE have; RETURN res; END; $$ LANGUAGE plpgsql; -- results_eq( sql, cursor ) CREATE OR REPLACE FUNCTION results_eq( TEXT, refcursor ) RETURNS TEXT AS $$ SELECT results_eq( $1, $2, NULL::text ); $$ LANGUAGE sql; -- results_eq( cursor, sql, description ) CREATE OR REPLACE FUNCTION results_eq( refcursor, TEXT, TEXT ) RETURNS TEXT AS $$ DECLARE want REFCURSOR; res TEXT; BEGIN OPEN want FOR EXECUTE _query($2); res := results_eq($1, want, $3); CLOSE want; RETURN res; END; $$ LANGUAGE plpgsql; -- results_eq( cursor, sql ) CREATE OR REPLACE FUNCTION results_eq( refcursor, TEXT ) RETURNS TEXT AS $$ SELECT results_eq( $1, $2, NULL::text ); $$ LANGUAGE sql; -- results_eq( cursor, array, description ) CREATE OR REPLACE FUNCTION results_eq( refcursor, anyarray, TEXT ) RETURNS TEXT AS $$ DECLARE want REFCURSOR; res TEXT; BEGIN OPEN want FOR SELECT $2[i] FROM generate_series(array_lower($2, 1), array_upper($2, 1)) s(i); res := results_eq($1, want, $3); CLOSE want; RETURN res; END; $$ LANGUAGE plpgsql; -- results_eq( cursor, array ) CREATE OR REPLACE FUNCTION results_eq( refcursor, anyarray ) RETURNS TEXT AS $$ SELECT results_eq( $1, $2, NULL::text ); $$ LANGUAGE sql; -- results_ne( cursor, cursor, description ) CREATE OR REPLACE FUNCTION results_ne( refcursor, refcursor, text ) RETURNS TEXT AS $$ DECLARE have ALIAS FOR $1; want ALIAS FOR $2; have_rec RECORD; want_rec RECORD; have_found BOOLEAN; want_found BOOLEAN; BEGIN FETCH have INTO have_rec; have_found := FOUND; FETCH want INTO want_rec; want_found := FOUND; WHILE have_found OR want_found LOOP IF have_rec::text IS DISTINCT FROM want_rec::text OR have_found <> want_found THEN RETURN ok( true, $3 ); ELSE FETCH have INTO have_rec; have_found := FOUND; FETCH want INTO want_rec; want_found := FOUND; END IF; END LOOP; RETURN ok( false, $3 ); EXCEPTION WHEN datatype_mismatch THEN RETURN ok( false, $3 ) || E'\n' || diag( E' Columns differ between queries:\n' || ' have: ' || CASE WHEN have_found THEN have_rec::text ELSE 'NULL' END || E'\n' || ' want: ' || CASE WHEN want_found THEN want_rec::text ELSE 'NULL' END ); END; $$ LANGUAGE plpgsql; -- results_ne( cursor, cursor ) CREATE OR REPLACE FUNCTION results_ne( refcursor, refcursor ) RETURNS TEXT AS $$ SELECT results_ne( $1, $2, NULL::text ); $$ LANGUAGE sql; -- results_ne( sql, sql, description ) CREATE OR REPLACE FUNCTION results_ne( TEXT, TEXT, TEXT ) RETURNS TEXT AS $$ DECLARE have REFCURSOR; want REFCURSOR; res TEXT; BEGIN OPEN have FOR EXECUTE _query($1); OPEN want FOR EXECUTE _query($2); res := results_ne(have, want, $3); CLOSE have; CLOSE want; RETURN res; END; $$ LANGUAGE plpgsql; -- results_ne( sql, sql ) CREATE OR REPLACE FUNCTION results_ne( TEXT, TEXT ) RETURNS TEXT AS $$ SELECT results_ne( $1, $2, NULL::text ); $$ LANGUAGE sql; -- results_ne( sql, array, description ) CREATE OR REPLACE FUNCTION results_ne( TEXT, anyarray, TEXT ) RETURNS TEXT AS $$ DECLARE have REFCURSOR; want REFCURSOR; res TEXT; BEGIN OPEN have FOR EXECUTE _query($1); OPEN want FOR SELECT $2[i] FROM generate_series(array_lower($2, 1), array_upper($2, 1)) s(i); res := results_ne(have, want, $3); CLOSE have; CLOSE want; RETURN res; END; $$ LANGUAGE plpgsql; -- results_ne( sql, array ) CREATE OR REPLACE FUNCTION results_ne( TEXT, anyarray ) RETURNS TEXT AS $$ SELECT results_ne( $1, $2, NULL::text ); $$ LANGUAGE sql; -- results_ne( sql, cursor, description ) CREATE OR REPLACE FUNCTION results_ne( TEXT, refcursor, TEXT ) RETURNS TEXT AS $$ DECLARE have REFCURSOR; res TEXT; BEGIN OPEN have FOR EXECUTE _query($1); res := results_ne(have, $2, $3); CLOSE have; RETURN res; END; $$ LANGUAGE plpgsql; -- results_ne( sql, cursor ) CREATE OR REPLACE FUNCTION results_ne( TEXT, refcursor ) RETURNS TEXT AS $$ SELECT results_ne( $1, $2, NULL::text ); $$ LANGUAGE sql; -- results_ne( cursor, sql, description ) CREATE OR REPLACE FUNCTION results_ne( refcursor, TEXT, TEXT ) RETURNS TEXT AS $$ DECLARE want REFCURSOR; res TEXT; BEGIN OPEN want FOR EXECUTE _query($2); res := results_ne($1, want, $3); CLOSE want; RETURN res; END; $$ LANGUAGE plpgsql; -- results_ne( cursor, sql ) CREATE OR REPLACE FUNCTION results_ne( refcursor, TEXT ) RETURNS TEXT AS $$ SELECT results_ne( $1, $2, NULL::text ); $$ LANGUAGE sql; -- results_ne( cursor, array, description ) CREATE OR REPLACE FUNCTION results_ne( refcursor, anyarray, TEXT ) RETURNS TEXT AS $$ DECLARE want REFCURSOR; res TEXT; BEGIN OPEN want FOR SELECT $2[i] FROM generate_series(array_lower($2, 1), array_upper($2, 1)) s(i); res := results_ne($1, want, $3); CLOSE want; RETURN res; END; $$ LANGUAGE plpgsql; -- results_ne( cursor, array ) CREATE OR REPLACE FUNCTION results_ne( refcursor, anyarray ) RETURNS TEXT AS $$ SELECT results_ne( $1, $2, NULL::text ); $$ LANGUAGE sql; -- isa_ok( value, regtype, description ) CREATE OR REPLACE FUNCTION isa_ok( anyelement, regtype, TEXT ) RETURNS TEXT AS $$ DECLARE typeof regtype := pg_typeof($1); BEGIN IF typeof = $2 THEN RETURN ok(true, $3 || ' isa ' || $2 ); END IF; RETURN ok(false, $3 || ' isa ' || $2 ) || E'\n' || diag(' ' || $3 || ' isn''t a "' || $2 || '" it''s a "' || typeof || '"'); END; $$ LANGUAGE plpgsql; -- isa_ok( value, regtype ) CREATE OR REPLACE FUNCTION isa_ok( anyelement, regtype ) RETURNS TEXT AS $$ SELECT isa_ok($1, $2, 'the value'); $$ LANGUAGE sql; -- is_empty( sql, description ) CREATE OR REPLACE FUNCTION is_empty( TEXT, TEXT ) RETURNS TEXT AS $$ DECLARE extras TEXT[] := '{}'; res BOOLEAN := TRUE; msg TEXT := ''; rec RECORD; BEGIN -- Find extra records. FOR rec in EXECUTE _query($1) LOOP extras := extras || rec::text; END LOOP; -- What extra records do we have? IF extras[1] IS NOT NULL THEN res := FALSE; msg := E'\n' || diag( E' Unexpected records:\n ' || array_to_string( extras, E'\n ' ) ); END IF; RETURN ok(res, $2) || msg; END; $$ LANGUAGE plpgsql; -- is_empty( sql ) CREATE OR REPLACE FUNCTION is_empty( TEXT ) RETURNS TEXT AS $$ SELECT is_empty( $1, NULL ); $$ LANGUAGE sql; -- collect_tap( tap, tap, tap ) CREATE OR REPLACE FUNCTION collect_tap( text[] ) RETURNS TEXT AS $$ SELECT array_to_string($1, E'\n'); $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION _tlike ( BOOLEAN, TEXT, TEXT, TEXT ) RETURNS TEXT AS $$ SELECT ok( $1, $4 ) || CASE WHEN $1 THEN '' ELSE E'\n' || diag( ' error message: ' || COALESCE( quote_literal($2), 'NULL' ) || E'\n doesn''t match: ' || COALESCE( quote_literal($3), 'NULL' ) ) END; $$ LANGUAGE sql; -- throws_like ( sql, pattern, description ) CREATE OR REPLACE FUNCTION throws_like ( TEXT, TEXT, TEXT ) RETURNS TEXT AS $$ BEGIN EXECUTE _query($1); RETURN ok( FALSE, $3 ) || E'\n' || diag( ' no exception thrown' ); EXCEPTION WHEN OTHERS THEN return _tlike( SQLERRM ~~ $2, SQLERRM, $2, $3 ); END; $$ LANGUAGE plpgsql; -- throws_like ( sql, pattern ) CREATE OR REPLACE FUNCTION throws_like ( TEXT, TEXT ) RETURNS TEXT AS $$ SELECT throws_like($1, $2, 'Should throw exception like ' || quote_literal($2) ); $$ LANGUAGE sql; -- throws_ilike ( sql, pattern, description ) CREATE OR REPLACE FUNCTION throws_ilike ( TEXT, TEXT, TEXT ) RETURNS TEXT AS $$ BEGIN EXECUTE _query($1); RETURN ok( FALSE, $3 ) || E'\n' || diag( ' no exception thrown' ); EXCEPTION WHEN OTHERS THEN return _tlike( SQLERRM ~~* $2, SQLERRM, $2, $3 ); END; $$ LANGUAGE plpgsql; -- throws_ilike ( sql, pattern ) CREATE OR REPLACE FUNCTION throws_ilike ( TEXT, TEXT ) RETURNS TEXT AS $$ SELECT throws_ilike($1, $2, 'Should throw exception like ' || quote_literal($2) ); $$ LANGUAGE sql; -- throws_matching ( sql, pattern, description ) CREATE OR REPLACE FUNCTION throws_matching ( TEXT, TEXT, TEXT ) RETURNS TEXT AS $$ BEGIN EXECUTE _query($1); RETURN ok( FALSE, $3 ) || E'\n' || diag( ' no exception thrown' ); EXCEPTION WHEN OTHERS THEN return _tlike( SQLERRM ~ $2, SQLERRM, $2, $3 ); END; $$ LANGUAGE plpgsql; -- throws_matching ( sql, pattern ) CREATE OR REPLACE FUNCTION throws_matching ( TEXT, TEXT ) RETURNS TEXT AS $$ SELECT throws_matching($1, $2, 'Should throw exception matching ' || quote_literal($2) ); $$ LANGUAGE sql; -- throws_imatching ( sql, pattern, description ) CREATE OR REPLACE FUNCTION throws_imatching ( TEXT, TEXT, TEXT ) RETURNS TEXT AS $$ BEGIN EXECUTE _query($1); RETURN ok( FALSE, $3 ) || E'\n' || diag( ' no exception thrown' ); EXCEPTION WHEN OTHERS THEN return _tlike( SQLERRM ~* $2, SQLERRM, $2, $3 ); END; $$ LANGUAGE plpgsql; -- throws_imatching ( sql, pattern ) CREATE OR REPLACE FUNCTION throws_imatching ( TEXT, TEXT ) RETURNS TEXT AS $$ SELECT throws_imatching($1, $2, 'Should throw exception matching ' || quote_literal($2) ); $$ LANGUAGE sql; -- roles_are( roles[], description ) CREATE OR REPLACE FUNCTION _dexists ( NAME, NAME ) RETURNS BOOLEAN AS $$ SELECT EXISTS( SELECT true FROM pg_catalog.pg_namespace n JOIN pg_catalog.pg_type t on n.oid = t.typnamespace WHERE n.nspname = $1 AND t.typname = $2 ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _dexists ( NAME ) RETURNS BOOLEAN AS $$ SELECT EXISTS( SELECT true FROM pg_catalog.pg_type t WHERE t.typname = $1 AND pg_catalog.pg_type_is_visible(t.oid) ); $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION _get_dtype( NAME, TEXT, BOOLEAN ) RETURNS TEXT AS $$ SELECT display_type(CASE WHEN $3 THEN tn.nspname ELSE NULL END, t.oid, t.typtypmod) FROM pg_catalog.pg_type d JOIN pg_catalog.pg_namespace dn ON d.typnamespace = dn.oid JOIN pg_catalog.pg_type t ON d.typbasetype = t.oid JOIN pg_catalog.pg_namespace tn ON d.typnamespace = tn.oid WHERE d.typisdefined AND dn.nspname = $1 AND d.typname = LOWER($2) AND d.typtype = 'd' $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION _get_dtype( NAME ) RETURNS TEXT AS $$ SELECT display_type(t.oid, t.typtypmod) FROM pg_catalog.pg_type d JOIN pg_catalog.pg_type t ON d.typbasetype = t.oid WHERE d.typisdefined AND d.typname = LOWER($1) AND d.typtype = 'd' $$ LANGUAGE sql; -- domain_type_is( schema, domain, schema, type, description ) CREATE OR REPLACE FUNCTION domain_type_is( NAME, TEXT, NAME, TEXT, TEXT ) RETURNS TEXT AS $$ DECLARE actual_type TEXT := _get_dtype($1, $2, true); BEGIN IF actual_type IS NULL THEN RETURN fail( $5 ) || E'\n' || diag ( ' Domain ' || quote_ident($1) || '.' || $2 || ' does not exist' ); END IF; RETURN is( actual_type, quote_ident($3) || '.' || _quote_ident_like($4, actual_type), $5 ); END; $$ LANGUAGE plpgsql; -- domain_type_is( schema, domain, schema, type ) CREATE OR REPLACE FUNCTION domain_type_is( NAME, TEXT, NAME, TEXT ) RETURNS TEXT AS $$ SELECT domain_type_is( $1, $2, $3, $4, 'Domain ' || quote_ident($1) || '.' || $2 || ' should extend type ' || quote_ident($3) || '.' || $4 ); $$ LANGUAGE SQL; -- domain_type_is( schema, domain, type, description ) CREATE OR REPLACE FUNCTION domain_type_is( NAME, TEXT, TEXT, TEXT ) RETURNS TEXT AS $$ DECLARE actual_type TEXT := _get_dtype($1, $2, false); BEGIN IF actual_type IS NULL THEN RETURN fail( $4 ) || E'\n' || diag ( ' Domain ' || quote_ident($1) || '.' || $2 || ' does not exist' ); END IF; RETURN is( actual_type, _quote_ident_like($3, actual_type), $4 ); END; $$ LANGUAGE plpgsql; -- domain_type_is( schema, domain, type ) CREATE OR REPLACE FUNCTION domain_type_is( NAME, TEXT, TEXT ) RETURNS TEXT AS $$ SELECT domain_type_is( $1, $2, $3, 'Domain ' || quote_ident($1) || '.' || $2 || ' should extend type ' || $3 ); $$ LANGUAGE SQL; -- domain_type_is( domain, type, description ) CREATE OR REPLACE FUNCTION domain_type_is( TEXT, TEXT, TEXT ) RETURNS TEXT AS $$ DECLARE actual_type TEXT := _get_dtype($1); BEGIN IF actual_type IS NULL THEN RETURN fail( $3 ) || E'\n' || diag ( ' Domain ' || $1 || ' does not exist' ); END IF; RETURN is( actual_type, _quote_ident_like($2, actual_type), $3 ); END; $$ LANGUAGE plpgsql; -- domain_type_is( domain, type ) CREATE OR REPLACE FUNCTION domain_type_is( TEXT, TEXT ) RETURNS TEXT AS $$ SELECT domain_type_is( $1, $2, 'Domain ' || $1 || ' should extend type ' || $2 ); $$ LANGUAGE SQL; -- domain_type_isnt( schema, domain, schema, type, description ) CREATE OR REPLACE FUNCTION domain_type_isnt( NAME, TEXT, NAME, TEXT, TEXT ) RETURNS TEXT AS $$ DECLARE actual_type TEXT := _get_dtype($1, $2, true); BEGIN IF actual_type IS NULL THEN RETURN fail( $5 ) || E'\n' || diag ( ' Domain ' || quote_ident($1) || '.' || $2 || ' does not exist' ); END IF; RETURN isnt( actual_type, quote_ident($3) || '.' || _quote_ident_like($4, actual_type), $5 ); END; $$ LANGUAGE plpgsql; -- domain_type_isnt( schema, domain, schema, type ) CREATE OR REPLACE FUNCTION domain_type_isnt( NAME, TEXT, NAME, TEXT ) RETURNS TEXT AS $$ SELECT domain_type_isnt( $1, $2, $3, $4, 'Domain ' || quote_ident($1) || '.' || $2 || ' should not extend type ' || quote_ident($3) || '.' || $4 ); $$ LANGUAGE SQL; -- domain_type_isnt( schema, domain, type, description ) CREATE OR REPLACE FUNCTION domain_type_isnt( NAME, TEXT, TEXT, TEXT ) RETURNS TEXT AS $$ DECLARE actual_type TEXT := _get_dtype($1, $2, false); BEGIN IF actual_type IS NULL THEN RETURN fail( $4 ) || E'\n' || diag ( ' Domain ' || quote_ident($1) || '.' || $2 || ' does not exist' ); END IF; RETURN isnt( actual_type, _quote_ident_like($3, actual_type), $4 ); END; $$ LANGUAGE plpgsql; -- domain_type_isnt( schema, domain, type ) CREATE OR REPLACE FUNCTION domain_type_isnt( NAME, TEXT, TEXT ) RETURNS TEXT AS $$ SELECT domain_type_isnt( $1, $2, $3, 'Domain ' || quote_ident($1) || '.' || $2 || ' should not extend type ' || $3 ); $$ LANGUAGE SQL; -- domain_type_isnt( domain, type, description ) CREATE OR REPLACE FUNCTION domain_type_isnt( TEXT, TEXT, TEXT ) RETURNS TEXT AS $$ DECLARE actual_type TEXT := _get_dtype($1); BEGIN IF actual_type IS NULL THEN RETURN fail( $3 ) || E'\n' || diag ( ' Domain ' || $1 || ' does not exist' ); END IF; RETURN isnt( actual_type, _quote_ident_like($2, actual_type), $3 ); END; $$ LANGUAGE plpgsql; -- domain_type_isnt( domain, type ) CREATE OR REPLACE FUNCTION domain_type_isnt( TEXT, TEXT ) RETURNS TEXT AS $$ SELECT domain_type_isnt( $1, $2, 'Domain ' || $1 || ' should not extend type ' || $2 ); $$ LANGUAGE SQL; -- row_eq( sql, record, description ) CREATE OR REPLACE FUNCTION row_eq( TEXT, anyelement, TEXT ) RETURNS TEXT AS $$ DECLARE rec RECORD; BEGIN EXECUTE _query($1) INTO rec; IF NOT rec::text IS DISTINCT FROM $2::text THEN RETURN ok(true, $3); END IF; RETURN ok(false, $3 ) || E'\n' || diag( ' have: ' || CASE WHEN rec IS NULL THEN 'NULL' ELSE rec::text END || E'\n want: ' || CASE WHEN $2 IS NULL THEN 'NULL' ELSE $2::text END ); END; $$ LANGUAGE plpgsql; -- row_eq( sql, record ) CREATE OR REPLACE FUNCTION row_eq( TEXT, anyelement ) RETURNS TEXT AS $$ SELECT row_eq($1, $2, NULL ); $$ LANGUAGE sql; -- triggers_are( schema, table, triggers[], description ) pg-semver-0.32.1/test/sql/000077500000000000000000000000001446231066500152675ustar00rootroot00000000000000pg-semver-0.32.1/test/sql/base.sql000066400000000000000000000440501446231066500167250ustar00rootroot00000000000000\set ECHO none BEGIN; \i test/pgtap-core.sql \i sql/semver.sql SELECT plan(334); --SELECT * FROM no_plan(); SELECT has_type('semver'); SELECT is( NULL::semver, NULL, 'semvers should be NULLable' ); SELECT lives_ok( $$ SELECT '$$ || v || $$'::semver $$, '"' || v || '" is a valid semver' ) FROM unnest(ARRAY[ '1.2.2', '0.2.2', '0.0.0', '0.1.999', '9999.9999999.823823', '1.0.0-beta1', '1.0.0-beta2', '1.0.0', '1.0.0-1', '1.0.0-alpha+d34dm34t', '1.0.0+d34dm34t', '20110204.0.0', '1.0.0-alpha.0a', '1.0.0+010', '1.0.0+alpha.010', '1.0.0-0AEF' ]) AS v; SELECT throws_ok( $$ SELECT '$$ || v || $$'::semver $$, NULL, '"' || v || '" is not a valid semver' ) FROM unnest(ARRAY[ '1.2', '1.2.02', '1.2.2-', '1.2.3b#5', '03.3.3', 'v1.2.2', '1.3b', '1.4b.0', '1v', '1v.2.2v', '1.2.4b.5', '1.0.0-alpha.010', '1.0.0-02799', '1.1.2+.123', '1.1.2-.123', '1.2.3-ñø', '1.2.3+ñø1' ]) AS v; -- Test =, <=, and >=. SELECT collect_tap(ARRAY[ ok(semver_cmp(lv::semver, rv::semver) = 0, 'semver(' || lv || ', ' || rv || ') should = 0'), ok(lv::semver = rv::semver, 'v' || lv || ' should = v' || rv), ok(lv::semver <= rv::semver, 'v' || lv || ' should be <= v' || rv), ok(lv::semver >= rv::semver, 'v' || lv || ' should be >= v' || rv) ]) FROM (VALUES ('1.2.2', '1.2.2'), ('1.2.23', '1.2.23'), ('0.0.0', '0.0.0'), ('999.888.7777', '999.888.7777'), ('0.1.2-beta3', '0.1.2-beta3') ) AS f(lv, rv); -- Test semver <> semver SELECT collect_tap(ARRAY[ ok(semver_cmp(lv::semver, rv::semver) <> 0, 'semver(' || lv || ', ' || rv || ') should <> 0'), ok(lv::semver <> rv::semver, 'v' || lv || ' should not equal v' || rv) ]) FROM (VALUES ('1.2.2', '1.2.3'), ('0.0.1', '1.0.0'), ('1.0.1', '1.1.0'), ('1.1.1', '1.1.0'), ('1.2.3-b', '1.2.3'), ('1.2.3', '1.2.3-b'), ('1.2.3-a', '1.2.3-b'), ('1.2.3-aaaaaaa1', '1.2.3-aaaaaaa2'), ('1.2.3-1.2.3', '1.2.3-1.2.3.4') ) AS f(lv, rv); -- Test >, >=, <, and <=. SELECT collect_tap(ARRAY[ ok( semver_cmp(lv::semver, rv::semver) > 0, 'semver(' || lv || ', ' || rv || ') should > 0'), ok( semver_cmp(rv::semver, lv::semver) < 0, 'semver(' || rv || ', ' || lv || ') should < 0'), ok(lv::semver > rv::semver, 'v' || lv || ' should be > v' || rv), ok(lv::semver >= rv::semver, 'v' || lv || ' should be >= v' || rv), ok(rv::semver < lv::semver, 'v' || rv || ' should be < v' || lv), ok(rv::semver <= lv::semver, 'v' || rv || ' should be <= v' || lv) ]) FROM (VALUES ('2.2.2', '1.1.1'), ('2.2.2', '2.1.1'), ('2.2.2', '2.2.1'), ('2.2.2-b', '2.2.1'), ('2.2.2', '2.2.2-b'), ('2.2.2-c', '2.2.2-b'), ('2.2.2-rc-2', '2.2.2-RC-1'), ('2.2.2-rc-1', '2.2.2-RC-1'), ('0.9.10', '0.9.9'), ('1.0.1-1.2.3', '1.0.1-0.9.9.9') ) AS f(lv, rv); -- Test to_semver(). SELECT has_function('to_semver'); SELECT has_function('to_semver', ARRAY['text']); SELECT function_returns('to_semver', 'semver'); SELECT is( to_semver(dirty), clean::semver, 'to_semver(' || dirty || ') should return ' || clean ) FROM (VALUES ('1.2.2', '1.2.2'), ('01.2.2', '1.2.2'), ('1.02.2', '1.2.2'), ('1.2.02', '1.2.2'), ('1.2.02b', '1.2.2-b'), ('1.2.02beta-3 ', '1.2.2-beta-3'), ('1.02.02rc1', '1.2.2-rc1'), ('1.0', '1.0.0'), ('1', '1.0.0'), ('.0.02', '0.0.2'), ('1..02', '1.0.2'), ('1..', '1.0.0'), ('1.1', '1.1.0'), ('1.2.b1', '1.2.0-b1'), ('9.0beta4', '9.0.0-beta4'), -- PostgreSQL format. ('9b', '9.0.0-b'), ('rc1', '0.0.0-rc1'), ('', '0.0.0'), ('..2', '0.0.2'), ('1.2.3 a', '1.2.3-a'), ('..2 b', '0.0.2-b'), (' 012.2.2', '12.2.2'), ('20110204', '20110204.0.0') ) v(dirty, clean); SELECT is( to_semver(clean), clean::semver, 'to_semver(' || clean || ') should return incoming text' ) FROM (VALUES ('1.0.0-alpha'), -- SV2 9. ('1.0.0-alpha.1'), -- SV2 9. ('1.0.0-0.3.7'), -- SV2 9. ('1.0.0-x.7.z.92'), -- SV2 9. ('1.0.0-alpha+001'), -- SV2 10. ('1.0.0+20130313144700'), -- SV2 10. ('1.0.0-beta+exp.sha.5114f85') -- SV2 10. ) v(clean); -- to_semver still needs to reject truly bad input SELECT throws_ok( $$ SELECT '$$ || v || $$'::semver $$, NULL, '"' || v || '" is not a valid semver' ) FROM unnest(ARRAY[ '1.2.0 beta 4', '1.2.2-', '1.2.3b#5', 'v1.2.2', '1.4b.0', '1v.2.2v', '1.2.4b.5', '1.2.3.4', '1.2.3 4', '1.2000000000000000.3.4' ]) AS v; -- Test sort ordering CREATE TABLE vs ( version semver ); INSERT INTO vs VALUES ('1.2.0'), ('1.0.0'), ('1.0.0-p0'), ('0.9.9'), ('0.9.10'); SELECT is(max(version), '1.2.0', 'max(semver) should work') FROM vs; SELECT is(min(version), '0.9.9', 'min(semver) should work') FROM vs; SELECT results_eq( $$ SELECT version FROM vs ORDER BY version USING < $$, $$ VALUES ('0.9.9'::semver), ('0.9.10'::semver), ('1.0.0-p0'::semver), ('1.0.0'::semver), ('1.2.0'::semver) $$, 'ORDER BY semver USING < should work' ); SELECT results_eq( $$ SELECT version FROM vs ORDER BY version USING > $$, $$ VALUES ('1.2.0'::semver), ('1.0.0'::semver), ('1.0.0-p0'::semver), ('0.9.10'::semver), ('0.9.9'::semver) $$, 'ORDER BY semver USING > should work' ); -- Test constructors. SELECT is( text('1.2.0'::semver), '1.2.0', 'construct to text' ); SELECT is( semver('1.2.0'), '1.2.0'::semver, 'construct from text' ); SELECT is( semver(1.2), '1.2.0'::semver, 'construct from bare number' ); SELECT is( semver(1.2::numeric), '1.2.0'::semver, 'construct from numeric' ); SELECT is( semver(1), '1.0.0'::semver, 'construct from bare integer' ); SELECT is( semver(1::integer), '1.0.0'::semver, 'construct from integer' ); SELECT is( semver(1::bigint), '1.0.0'::semver, 'construct from bigint' ); SELECT is( semver(1::smallint), '1.0.0'::semver, 'construct from smallint' ); SELECT is( semver(1.2::decimal), '1.2.0'::semver, 'construct from decimal' ); SELECT is( semver(1.2::real), '1.2.0'::semver, 'construct from real' ); SELECT is( semver(1.2::double precision), '1.2.0'::semver, 'construct from double' ); SELECT is( semver(1.2::float), '1.2.0'::semver, 'construct from float' ); -- Test casting. SELECT is( semver('1.2.0'::text), '1.2.0', 'cast to text' ); SELECT is( text('1.2.0')::semver, '1.2.0'::semver, 'cast from text' ); SELECT is( 1::semver, '1.0.0'::semver, 'Cast from bare integer'); SELECT is( 1.2::semver, '1.2.0'::semver, 'Cast from bare number'); SELECT is( 1.2::numeric::semver, '1.2.0'::semver, 'Cast from numeric'); SELECT is( 1::integer::semver, '1.0.0'::semver, 'Cast from integer'); SELECT is( 1::bigint::semver, '1.0.0'::semver, 'Cast from bigint'); SELECT is( 1::smallint::semver, '1.0.0'::semver, 'Cast from smallint'); SELECT is( 1.0::decimal::semver, '1.0.0'::semver, 'Cast from decimal'); SELECT is( 1::decimal::semver, '1.0.0'::semver, 'Cast from decimal'); SELECT is( 1.0::real::semver, '1.0.0'::semver, 'Cast from real'); SELECT is( 1.0::double precision::semver, '1.0.0'::semver, 'Cast from double precision'); SELECT is( 1.0::float::semver, '1.0.0'::semver, 'Cast from float'); -- Test casting some more. SELECT IS(lv::text, rv, 'Should correctly cast "' || rv || '" to text') FROM (VALUES ('1.0.0-beta'::semver, '1.0.0-beta'), ('1.0.0-beta1'::semver, '1.0.0-beta1'), ('1.0.0-alpha'::semver, '1.0.0-alpha'), ('1.0.0-alph'::semver, '1.0.0-alph'), ('1.0.0-food'::semver, '1.0.0-food'), ('1.0.0-f111'::semver, '1.0.0-f111'), ('1.0.0-f111asbcdasdfasdfasdfasdfasdfasdffasdfadsf'::semver, '1.0.0-f111asbcdasdfasdfasdfasdfasdfasdffasdfadsf') ) AS f(lv, rv); -- SEMV 2.0.0 tests. SELECT lives_ok( $$ SELECT '$$ || v || $$'::semver $$, '"' || v || '" is a valid 2.0.0 semver' ) FROM unnest(ARRAY[ '1.0.0+1', '1.0.0-1+1', '1.0.0-1.1+1', '1.0.0-1.1.1.1.1.1.1.1.1.1.1+1.1.1.1.1.1.1.1', '1.0.0-1.2', '1.0.0-1.0.2', '1.0.0-alpha', '1.0.0-alpha.1', '1.0.0-0.3.7', '1.0.0-x.7.z.92', '0.2.13+1583426134.07de632' ]) AS v; SELECT throws_ok( $$ SELECT '$$ || v || $$'::semver $$, NULL, '"' || v || '" is not a valid 2.0.0 semver' ) FROM unnest(ARRAY[ '1.0.0-a..', '1.0.0-a.1.', '1.0.0+1_1', '1.0.0-1....', '1.0.0-1_2', '1.0.0-1.02' ]) AS v; DELETE FROM vs; INSERT INTO vs VALUES ('0.9.9-a1.1+1234'::semver), ('0.9.9-a1.2.3'::semver), ('0.9.9-a1.2'::semver), ('0.9.9'::semver), ('1.0.0+99'::semver), ('1.0.0-1'::semver); SELECT results_eq( $$ SELECT version FROM vs ORDER BY version USING < $$, $$ VALUES ('0.9.9-a1.1+1234'::semver), ('0.9.9-a1.2'::semver), ('0.9.9-a1.2.3'::semver), ('0.9.9'::semver), ('1.0.0-1'::semver), ('1.0.0+99'::semver) $$, 'ORDER BY semver (2.0.0) USING < should work' ); SELECT results_eq( $$ SELECT version FROM vs ORDER BY version USING > $$, $$ VALUES ('1.0.0+99'::semver), ('1.0.0-1'::semver), ('0.9.9'::semver), ('0.9.9-a1.2.3'::semver), ('0.9.9-a1.2'::semver), ('0.9.9-a1.1+1234'::semver) $$, 'ORDER BY semver (2.0.0) USING > should work' ); SELECT collect_tap(ARRAY[ ok(semver_cmp(lv::semver, rv::semver) = 0, 'semver(' || lv || ', ' || rv || ') should = 0'), ok(lv::semver = rv::semver, 'v' || lv || ' should = v' || rv), ok(lv::semver <= rv::semver, 'v' || lv || ' should be <= v' || rv), ok(lv::semver >= rv::semver, 'v' || lv || ' should be >= v' || rv) ]) FROM (VALUES ('1.0.0-1+1', '1.0.0-1+5'), ('1.0.0-1.1+1', '1.0.0-1.1+5') ) AS f(lv, rv); -- Regressions. SELECT is( to_semver(lv)::TEXT, rv, 'Should correctly represent "' || lv || '" as "' || rv || '"' ) FROM (VALUES ('0.5.0-release1', '0.5.0-release1'), ('0.5.0release1', '0.5.0-release1'), ('0.5-release1', '0.5.0-release1'), ('0.5release1', '0.5.0-release1'), ('0.5-1', '0.5.0-1'), ('1.2.3-1.02', '1.2.3-1.2') ) AS f(lv, rv); -- Test is_semver(). SELECT has_function('is_semver'); SELECT has_function('is_semver', ARRAY['text']); SELECT function_returns('is_semver', 'boolean'); SELECT is( is_semver(stimulus), expected, 'is_semver(' || stimulus || ') should return ' || expected::text ) FROM (VALUES ('1.2.2', true), ('0.2.2', true), ('0.0.0', true), ('0.1.999', true), ('9999.9999999.823823', true), ('1.0.0-beta1', true), ('1.0.0-beta2', true), ('1.0.0', true), ('1.0.0-1', true), ('1.0.0-alpha+d34dm34t', true), ('1.0.0+d34dm34t', true), ('20110204.0.0', true), ('1.2', false), ('1.2.02', false), ('1.2.2-', false), ('1.2.3b#5', false), ('03.3.3', false), ('v1.2.2', false), ('1.3b', false), ('1.4b.0', false), ('1v', false), ('1v.2.2v', false), ('1.2.4b.5', false), ('2016.5.18-MYW-600', true), ('1010.5.0+2016-05-27-1832', true), ('0.2.13+1583426134.07de632', true) ) v(stimulus, expected); -- issue-gh-23 SELECT lives_ok( $$ SELECT '$$ || v || $$'::semver $$, '"' || v || '" is a valid semver' ) FROM unnest(ARRAY[ '2.3.0+80' ]) AS v; SELECT is( to_semver(dirty), clean::semver, 'to_semver(' || dirty || ') should return ' || clean ) FROM (VALUES ('2.3.0+80', '2.3.0+80') ) v(dirty, clean); SELECT is(lv::text, rv, 'Should correctly cast "' || rv || '" to text') FROM (VALUES ('2.3.0+80'::semver, '2.3.0+80') ) AS f(lv, rv); SELECT isnt(lv::semver > rv::semver, true, '"' || lv || '" > "' || rv || '" (NOT!)') FROM (VALUES ('2.3.0+80', '2.3.0+110') ) AS f(lv, rv); SELECT is(lv::semver > rv::semver, true, '"' || lv || '" > "' || rv || '"') FROM (VALUES ('2.3.0+80', '2.3.0-alpha+110') ) AS f(lv, rv); CREATE TABLE vs23 ( version semver ); INSERT INTO vs23 VALUES ('1.0.0-alpha'), ('1.0.0-alpha.1'), ('1.0.0-alpha.beta'), ('1.0.0-beta'), ('1.0.0-beta.2'), ('1.0.0-beta.11'), ('1.0.0-rc.1'), ('1.0.0'); SELECT results_eq( $$ SELECT version FROM vs23 ORDER BY version USING < $$, $$ VALUES ('1.0.0-alpha'::semver), ('1.0.0-alpha.1'::semver), ('1.0.0-alpha.beta'::semver), ('1.0.0-beta'::semver), ('1.0.0-beta.2'::semver), ('1.0.0-beta.11'::semver), ('1.0.0-rc.1'::semver), ('1.0.0'::semver) $$, 'ORDER BY semver USING < should work (section 11)' ); SELECT results_eq( $$ SELECT version FROM vs23 ORDER BY version USING > $$, $$ VALUES ('1.0.0'::semver), ('1.0.0-rc.1'::semver), ('1.0.0-beta.11'::semver), ('1.0.0-beta.2'::semver), ('1.0.0-beta'::semver), ('1.0.0-alpha.beta'::semver), ('1.0.0-alpha.1'::semver), ('1.0.0-alpha'::semver) $$, 'ORDER BY semver USING > should work (section 11)' ); SELECT is(lv::semver = rv::semver, true, '"' || lv || '" = "' || rv || '"') FROM (VALUES ('1.0.0', '1.0.0+535') ) AS f(lv, rv); SELECT isnt(lv::semver < rv::semver, true, '"' || lv || '" < "' || rv || '" (NOT!)') FROM (VALUES ('1.0.0', '1.0.0+535') ) AS f(lv, rv); SELECT isnt(lv::semver > rv::semver, true, '"' || lv || '" > "' || rv || '" (NOT!)') FROM (VALUES ('1.0.0', '1.0.0+535') ) AS f(lv, rv); -- Test get_semver_major SELECT has_function('get_semver_major'); SELECT has_function('get_semver_major', 'semver'); SELECT function_returns('get_semver_major', 'integer'); SELECT is(get_semver_major('2.1.0-alpha'::semver), 2, 'major version check'); -- Test get_semver_minor SELECT has_function('get_semver_minor'); SELECT has_function('get_semver_minor', 'semver'); SELECT function_returns('get_semver_minor', 'integer'); SELECT is(get_semver_minor('2.1.0-alpha'::semver), 1, 'minor version check'); -- Test get_semver_patch SELECT has_function('get_semver_patch'); SELECT has_function('get_semver_patch', 'semver'); SELECT function_returns('get_semver_patch', 'integer'); SELECT is(get_semver_patch('2.1.0-alpha'::semver), 0, 'patch version check'); -- Test get_semver_prerelease SELECT has_function('get_semver_prerelease'); SELECT has_function('get_semver_prerelease', 'semver'); SELECT function_returns('get_semver_prerelease', 'text'); SELECT is(get_semver_prerelease('2.1.0-alpha'::semver), 'alpha', 'prerelease label check'); SELECT is(get_semver_prerelease('2.1.0-alpha+build'::semver), 'alpha', 'prerelease label check. must return prerelease only'); SELECT is(get_semver_prerelease('2.1.0+build'::semver), '', 'prerelease label check. must return empty string'); -- Test range type. SELECT ok( '1.0.0'::semver <@ '[1.0.0, 2.0.0]'::semverrange, '1.0.0 should be in range [1.0.0, 2.0.0]' ); SELECT ok( NOT '1.0.0'::semver <@ '[1.0.1, 2.0.0]'::semverrange, '1.0.0 should not be in range [1.0.1, 2.0.0]' ); SELECT ok( NOT semverrange('1.0.0', '2.0.0') @> '2.0.0'::semver, '2.0.0 should not be in range [1.0.1, 2.0.0)' ); SELECT ok( semverrange('1.0.0', '2.0.0') @> '1.9999.9999'::semver, '1.9999.9999 should be in range [1.0.1, 2.0.0)' ); SELECT ok( '1000.0.0'::semver <@ '[1.0.0,]'::semverrange, '1000.0.0 should be in range [1.0.0,)' ); SELECT bag_eq($$ SELECT version, version <@ ANY( '{"(1.0.0,1.2.3)", "(1.2.3,1.4.5)", "(1.4.5,2.0.0)"}'::semverrange[] ) AS valid FROM (VALUES ('1.0.0'::semver), ('1.0.1'), ('1.2.3'), ('1.2.4'), ('1.4.4'), ('1.4.5'), ('1.7.0'), ('2.0.0') ) AS v(version) $$, $$ VALUES ('1.0.0'::semver, false), ('1.0.1', true), ('1.2.3', false), ('1.2.4', true), ('1.4.4', true), ('1.4.5', false), ('1.7.0', true), ('2.0.0', false) $$, 'Should be able to work with arrays of semverranges'); -- Test formatting (issue-gh-48) SELECT is( '1.0.0+1234567890123456789012345'::semver::text, '1.0.0+1234567890123456789012345', 'Should properly format a 32 character semver' ); SELECT is( '1.0.0+12345678901234567890123456'::semver::text, '1.0.0+12345678901234567890123456', 'Should properly format a 33 character semver' ); -- issue-gh-46 SELECT is( '1.0.0-alpha-0'::semver::text, '1.0.0-alpha-0', 'Should propery format a prerelease with a hyphen' ); -- Make sure hashing works by forcing a hash aggregation. Also confirms -- that prerelease strings are compared case-sensitively. SET enable_sort=false; SELECT bag_eq( $$ SELECT DISTINCT v FROM (VALUES ('1.2.1'::SEMVER), ('1.2.1-RC1'), ('2.0.0-alph'), ('2.0.0-ALPH'), ('1.2.1'), ('9.20.30-OMG-1.23') ) v(v) $$, $$ (VALUES ('1.2.1'::SEMVER), ('1.2.1-RC1'), ('2.0.0-alph'), ('2.0.0-ALPH'), ('9.20.30-OMG-1.23') ) $$, 'Should get distinct values via hash aggregation' ); -- Test send and receive. SELECT has_function('semver_send'); SELECT has_function('semver_send', ARRAY['semver']); SELECT function_returns('semver_send', 'bytea'); SELECT has_function('semver_recv'); SELECT has_function('semver_recv', ARRAY['internal']); SELECT function_returns('semver_recv', 'semver'); -- Set up some values to copy. INSERT INTO vs VALUES ('1.2.2'::semver), ('9999.9999999.823823'), ('1.0.0-beta1'), ('1.0.0-1'), ('1.0.0-alpha+d34dm34t'), ('1.0.0+d34dm34t'), ('20110204.0.0'), ('1.0.0-0AEF'), (NULL) ; -- Test semver_send(). SELECT is( semver_send(version), '\x01' || version::bytea, 'semver_send(' || COALESCE(quote_literal(version::text), 'NULL') || ')' ) FROM vs; -- Cannot test semver_recv() directly, so instead copy to a file and back into -- another table. CREATE TABLE vs_recv(version semver); \copy vs TO semver_binary_copy.bin WITH BINARY; \copy vs_recv from semver_binary_copy.bin with binary; SELECT bag_eq( 'SELECT * FROM vs', 'SELECT * FROM vs_recv', 'Should have binary copied all of the semvers' ); SELECT * FROM finish(); ROLLBACK; pg-semver-0.32.1/test/sql/corpus.sql000066400000000000000000000046241446231066500173310ustar00rootroot00000000000000\set ECHO none BEGIN; -- Test the SemVer corpus from https://regex101.com/r/Ly7O1x/3/. \i test/pgtap-core.sql \i sql/semver.sql SELECT plan(71); --SELECT * FROM no_plan(); -- Valid Semantic Versions SELECT lives_ok( $$ SELECT '$$ || v || $$'::semver $$, '"' || v || '" is a valid semver' ) FROM unnest(ARRAY[ '0.0.4', '1.2.3', '10.20.30', '1.1.2-prerelease+meta', '1.1.2+meta', '1.1.2+meta-valid', '1.0.0-alpha', '1.0.0-beta', '1.0.0-alpha.beta', '1.0.0-alpha.beta.1', '1.0.0-alpha.1', '1.0.0-alpha0.valid', '1.0.0-alpha.0valid', '1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay', '1.0.0-rc.1+build.1', '2.0.0-rc.1+build.123', '1.2.3-beta', '10.2.3-DEV-SNAPSHOT', '1.2.3-SNAPSHOT-123', '1.0.0', '2.0.0', '1.1.7', '2.0.0+build.1848', '2.0.1-alpha.1227', '1.0.0-alpha+beta', '1.2.3----RC-SNAPSHOT.12.9.1--.12+788', '1.2.3----R-S.12.9.1--.12+meta', '1.2.3----RC-SNAPSHOT.12.9.1--.12', '1.0.0+0.build.1-rc.10000aaa-kk-0.1', '1.0.0-0A.is.legal' -- ]) AS v; SELECT todo('Large versions overflow integer bounds', 1); SELECT lives_ok( $$ SELECT '99999999999999999999999.999999999999999999.99999999999999999'::semver $$, '"99999999999999999999999.999999999999999999.99999999999999999" is a valid semver' ); -- Invalid Semantic Versions SELECT throws_ok( $$ SELECT '$$ || v || $$'::semver $$, NULL, '"' || v || '" is not a valid semver' ) FROM unnest(ARRAY[ '1', '1.2', '1.2.3-0123', '1.2.3-0123.0123', '1.1.2+.123', '+invalid', '-invalid', '-invalid+invalid', '-invalid.01', 'alpha', 'alpha.beta', 'alpha.beta.1', 'alpha.1', 'alpha+beta', 'alpha_beta', 'alpha.', 'alpha..', 'beta', '1.0.0-alpha_beta', '-alpha.', '1.0.0-alpha..', '1.0.0-alpha..1', '1.0.0-alpha...1', '1.0.0-alpha....1', '1.0.0-alpha.....1', '1.0.0-alpha......1', '1.0.0-alpha.......1', '01.1.1', '1.01.1', '1.1.01', '1.2', '1.2.3.DEV', '1.2-SNAPSHOT', '1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788', '1.2-RC-SNAPSHOT', '-1.0.3-gamma+b7718', '+justmeta', '9.8.7+meta+meta', '9.8.7-whatever+meta+meta', '99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12' ]) AS v; SELECT * FROM finish(); ROLLBACK;