├── CMakeLists.txt ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.sh ├── src ├── CMakeLists.txt ├── timestamp9--0.1.0--0.2.0.sql ├── timestamp9--0.2.0--0.3.0.sql ├── timestamp9--0.3.0--1.0.0.sql ├── timestamp9--1.0.0--1.0.1.sql ├── timestamp9--1.0.1--1.1.0.sql ├── timestamp9--1.1.0--1.2.0.sql ├── timestamp9--1.2.0--1.3.0.sql ├── timestamp9--1.3.0--1.4.0.sql ├── timestamp9.c ├── timestamp9.h └── timestamp9.sql ├── tests ├── CMakeLists.txt ├── README.md ├── expected │ ├── CMakeLists.txt │ └── basics.out ├── pg_regress.sh ├── postgresql.conf ├── runner.sh ├── runner_shared.sh ├── sql │ ├── CMakeLists.txt │ └── basics.sql └── test-defs.cmake ├── timestamp9.control └── version.config /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.4) 2 | 3 | configure_file("version.config" "version.config" COPYONLY) 4 | file(READ version.config VERSION_CONFIG) 5 | set(VERSION_REGEX "version[\t ]*=[\t ]*([0-9]+\\.[0-9]+\\.*[0-9]*)([-]([a-z]+))*\r?\nupdate_from_version[\t ]*=[\t ]*([0-9]+\\.[0-9]+\\.*[0-9]*)(\r?\n)*$") 6 | 7 | if (NOT (${VERSION_CONFIG} MATCHES ${VERSION_REGEX})) 8 | message(FATAL_ERROR "Cannot read version from version.config") 9 | endif () 10 | 11 | set(VERSION ${CMAKE_MATCH_1}) 12 | set(VERSION_MOD ${CMAKE_MATCH_3}) 13 | set(UPDATE_FROM_VERSION ${CMAKE_MATCH_4}) 14 | 15 | if (VERSION_MOD) 16 | set(PROJECT_VERSION_MOD ${VERSION}-${VERSION_MOD}) 17 | else () 18 | set(PROJECT_VERSION_MOD ${VERSION}) 19 | endif () 20 | 21 | if (NOT CMAKE_BUILD_TYPE) 22 | # Default to Release builds 23 | set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build, options are: None Debug Release RelWithDebInfo MinSizeRel" FORCE) 24 | endif () 25 | 26 | if (WIN32) 27 | if (NOT CMAKE_CONFIGURATION_TYPES) 28 | # Default to only include Release builds so MSBuild.exe 'just works' 29 | set(CMAKE_CONFIGURATION_TYPES Release CACHE STRING "Semicolon separated list of supported configuration types, only supports Debug, Release, MinSizeRel, and RelWithDebInfo, anything else will be ignored." FORCE) 30 | endif () 31 | endif (WIN32) 32 | 33 | project(timestamp9 VERSION ${VERSION}) 34 | 35 | message(STATUS "Timestamp9 version ${PROJECT_VERSION_MOD}. Can be updated from version ${UPDATE_FROM_VERSION}") 36 | message(STATUS "Build type is ${CMAKE_BUILD_TYPE}") 37 | 38 | # Search paths for Postgres binaries 39 | if (WIN32) 40 | find_path(PG_PATH 41 | bin/postgres 42 | HINTS 43 | "C:/PostgreSQL" 44 | "C:/Program Files/PostgreSQL" 45 | PATH_SUFFIXES 46 | bin 47 | 10/bin 48 | 96/bin 49 | pg96/bin 50 | DOC 51 | "The path to a PostgreSQL installation") 52 | endif (WIN32) 53 | 54 | if (UNIX) 55 | find_path(PG_PATH 56 | bin/postgres 57 | HINTS 58 | $ENV{HOME} 59 | /opt/local/pgsql 60 | /usr/local/pgsql 61 | /usr/lib/postgresql 62 | PATH_SUFFIXES 63 | bin 64 | 10/bin 65 | 9.6/bin 66 | 96/bin 67 | pg96/bin 68 | DOC 69 | "The path to a PostgreSQL installation") 70 | endif (UNIX) 71 | 72 | find_program(PG_CONFIG pg_config 73 | HINTS 74 | ${PG_PATH} 75 | PATH_SUFFIXES 76 | bin 77 | DOC 78 | "The path to the pg_config of the PostgreSQL version to compile against" 79 | REQUIRED) 80 | 81 | if (NOT PG_CONFIG) 82 | message(FATAL_ERROR "Unable to find 'pg_config'") 83 | endif () 84 | 85 | message(STATUS "Using pg_config ${PG_CONFIG}") 86 | 87 | # Check PostgreSQL version 88 | execute_process( 89 | COMMAND ${PG_CONFIG} --version 90 | OUTPUT_VARIABLE PG_VERSION_STRING 91 | OUTPUT_STRIP_TRAILING_WHITESPACE) 92 | 93 | if (NOT ${PG_VERSION_STRING} MATCHES "^PostgreSQL[ ]+([0-9]+)\\.?([0-9]*)(\\.([0-9]+))*(devel)?") 94 | message(FATAL_ERROR "Could not parse PostgreSQL version ${PG_VERSION_STRING}") 95 | endif () 96 | 97 | set(PG_VERSION_MAJOR ${CMAKE_MATCH_1}) 98 | set(PG_VERSION_MINOR ${CMAKE_MATCH_2}) 99 | set(PG_VERSION_PATCH ${CMAKE_MATCH_4}) 100 | 101 | if (NOT ${PG_VERSION_PATCH} OR ${PG_VERSION_PATCH} EQUAL "") 102 | set(PG_VERSION "${PG_VERSION_MAJOR}.${PG_VERSION_MINOR}") 103 | else () 104 | set(PG_VERSION "${PG_VERSION_MAJOR}.${PG_VERSION_MINOR}.${PG_VERSION_PATCH}") 105 | endif () 106 | 107 | message(STATUS "Compiling against PostgreSQL version ${PG_VERSION}") 108 | 109 | # Get PostgreSQL configuration from pg_config 110 | execute_process( 111 | COMMAND ${PG_CONFIG} --includedir 112 | OUTPUT_VARIABLE PG_INCLUDEDIR 113 | OUTPUT_STRIP_TRAILING_WHITESPACE) 114 | execute_process( 115 | COMMAND ${PG_CONFIG} --includedir-server 116 | OUTPUT_VARIABLE PG_INCLUDEDIR_SERVER 117 | OUTPUT_STRIP_TRAILING_WHITESPACE) 118 | execute_process( 119 | COMMAND ${PG_CONFIG} --libdir 120 | OUTPUT_VARIABLE PG_LIBDIR 121 | OUTPUT_STRIP_TRAILING_WHITESPACE) 122 | execute_process( 123 | COMMAND ${PG_CONFIG} --pkglibdir 124 | OUTPUT_VARIABLE PG_PKGLIBDIR 125 | OUTPUT_STRIP_TRAILING_WHITESPACE) 126 | execute_process( 127 | COMMAND ${PG_CONFIG} --sharedir 128 | OUTPUT_VARIABLE PG_SHAREDIR 129 | OUTPUT_STRIP_TRAILING_WHITESPACE) 130 | execute_process( 131 | COMMAND ${PG_CONFIG} --bindir 132 | OUTPUT_VARIABLE PG_BINDIR 133 | OUTPUT_STRIP_TRAILING_WHITESPACE) 134 | execute_process( 135 | COMMAND ${PG_CONFIG} --cppflags 136 | OUTPUT_VARIABLE PG_CPPFLAGS 137 | OUTPUT_STRIP_TRAILING_WHITESPACE) 138 | execute_process( 139 | COMMAND ${PG_CONFIG} --cflags 140 | OUTPUT_VARIABLE PG_CFLAGS 141 | OUTPUT_STRIP_TRAILING_WHITESPACE) 142 | execute_process( 143 | COMMAND ${PG_CONFIG} --ldflags 144 | OUTPUT_VARIABLE PG_LDFLAGS 145 | OUTPUT_STRIP_TRAILING_WHITESPACE) 146 | execute_process( 147 | COMMAND ${PG_CONFIG} --libs 148 | OUTPUT_VARIABLE PG_LIBS 149 | OUTPUT_STRIP_TRAILING_WHITESPACE) 150 | 151 | find_path(PG_SOURCE_DIR 152 | src/include/pg_config.h.in 153 | HINTS 154 | $ENV{HOME} 155 | $ENV{HOME}/projects 156 | $ENV{HOME}/Projects 157 | $ENV{HOME}/development 158 | $ENV{HOME}/Development 159 | $ENV{HOME}/workspace 160 | PATH_SUFFIXES 161 | postgres 162 | postgresql 163 | pgsql 164 | DOC 165 | "The path to the PostgreSQL source tree") 166 | 167 | if (PG_SOURCE_DIR) 168 | message(STATUS "Found PostgreSQL source in ${PG_SOURCE_DIR}") 169 | endif (PG_SOURCE_DIR) 170 | 171 | add_custom_target(controlfile ALL DEPENDS timestamp9.control) 172 | 173 | add_subdirectory(src) 174 | add_subdirectory(tests) 175 | 176 | install( 177 | FILES timestamp9.control 178 | DESTINATION "${PG_SHAREDIR}/extension") 179 | 180 | macro(get_parents DIR DIR_LIST) 181 | list(APPEND DIR_LIST ${DIR}) 182 | set(PDIR ${DIR}) 183 | while (NOT (${PDIR} STREQUAL "/")) 184 | list(APPEND DIR_LIST ${PDIR}) 185 | get_filename_component(PDIR ${PDIR} DIRECTORY) 186 | endwhile () 187 | endmacro() 188 | 189 | set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION_MOD}) 190 | set(CPACK_GENERATOR "RPM") 191 | set(CPACK_PACKAGE_NAME ${PROJECT_NAME}-PG${PG_VERSION_MAJOR}) 192 | set(CPACK_PACKAGE_RELEASE 1) 193 | set(CPACK_PACKAGE_CONTACT "Optiver") 194 | set(CPACK_PACKAGE_VENDOR "Optiver") 195 | set(CPACK_PACKAGING_INSTALL_PREFIX ${CMAKE_INSTALL_PREFIX}) 196 | set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-${CPACK_PACKAGE_RELEASE}.${CMAKE_SYSTEM_PROCESSOR}") 197 | get_parents(${PG_SHAREDIR}/extension DIR_LIST) 198 | get_parents(${PG_PKGLIBDIR} DIR_LIST) 199 | set(CPACK_RPM_EXCLUDE_FROM_AUTO_FILELIST_ADDITION ${DIR_LIST} "/usr/local") 200 | include(CPack) 201 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Timestamp9 welcomes contributions from everyone. Here are some guidelines that will help you get started: 4 | 5 | # Getting Started 6 | 7 | * Make sure you have a GitHub account. 8 | * Create an issue, or comment saying you're working on an existing issue. 9 | 10 | # Making Changes 11 | 12 | Contributions to timestamp9 or its dependencies should be made in the form of GitHub pull requests. Each pull request will be reviewed by a core developer and after some feedback, will be merged to master. All contributions follow this format, including those from core contributors. 13 | 14 | # Pull Request Checklist 15 | 16 | * [ ] Branch from master or rebase your branch to current master. 17 | * [ ] Each commit should compile and pass tests. 18 | * [ ] Add tests relevant to any fixed bug or new feature 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Optiver 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Extension timestamp9 2 | An efficient nanosecond precision timestamp type for Postgres 3 | 4 | ## Build & install 5 | ``` 6 | git clone https://github.com/optiver/timestamp9.git 7 | cd timestamp9 8 | mkdir build 9 | cd build 10 | cmake .. 11 | # or: cmake .. -DPG_CONFIG=/path/to/pg_config 12 | make 13 | sudo make install 14 | ``` 15 | 16 | ## Usage 17 | Internally, timestamp9 is stored in a 64-bit number as the number of nanoseconds since the UNIX epoch. This means the minimum and maximum representable time is: 18 | 19 | ``` 20 | postgres=# select 0::bigint::timestamp9; 21 | timestamp9 22 | ------------------------------------- 23 | 1970-01-01 01:00:00.000000000 +0100 24 | (1 row) 25 | 26 | postgres=# select 9223372036854775807::timestamp9; 27 | timestamp9 28 | ------------------------------------- 29 | 2262-04-12 01:47:16.854775807 +0200 30 | (1 row) 31 | 32 | ``` 33 | 34 | Timestamp input can be given either as the number of nanoseconds since Jan 1st 1970, which can be casted to timestamp9 as above, or it can be casted from text format. Both regular Postgres timestamptz text format, as well as a custom nanosecond text format are supported as inputs. 35 | ``` 36 | postgres=# select '2019-09-19 08:30:05.123456789 +0200'::timestamp9; 37 | timestamp9 38 | ------------------------------------- 39 | 2019-09-19 08:30:05.123456789 +0200 40 | (1 row) 41 | 42 | postgres=# select '2019-09-19 08:30:05'::timestamp9; 43 | timestamp9 44 | ------------------------------------- 45 | 2019-09-19 08:30:05.000000000 +0200 46 | (1 row) 47 | ``` 48 | 49 | A subset of the default operators and conversions is supported for timestamp9 types: 50 | - Cast from/to timestamp(tz) 51 | ``` 52 | postgres=# select now()::timestamp9::timestamptz::timestamp::timestamp9; 53 | now 54 | ------------------------------------- 55 | 2019-09-19 23:22:07.973781000 +0200 56 | (1 row) 57 | ``` 58 | - Cast from/to date 59 | ``` 60 | postgres=# select current_date::timestamp9; 61 | current_date 62 | ------------------------------------- 63 | 2019-09-19 00:00:00.000000000 +0200 64 | (1 row) 65 | ``` 66 | - Comparisons like greater than, less than etc. as well as use in btree/hash indices 67 | ``` 68 | postgres=# select '2019-09-19'::timestamp9 < '2019-09-20'::timestamp9, greatest(now()::timestamp9, '2019-01-01'::timestamp9); 69 | ?column? | greatest 70 | ----------+------------------------------------- 71 | t | 2019-09-19 23:27:21.364791000 +0200 72 | (1 row) 73 | ``` 74 | - Addition and subtraction of intervals 75 | ``` 76 | postgres=# select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 + interval '1d'; 77 | ?column? 78 | ------------------------------------- 79 | 2019-09-20 23:00:00.123456789 +0200 80 | (1 row) 81 | ``` 82 | 83 | # License 84 | 85 | --- 86 | 87 | Timestamp9 is: 88 | 89 | Copyright 2023 Optiver IP B.V. 90 | 91 | Licensed under the MIT License (the "License"); you may not use this file except in compliance 92 | with the License. You may obtain a copy of the License at 93 | 94 | ``` 95 | https://opensource.org/license/mit/ 96 | ``` 97 | 98 | Unless required by applicable law or explicitly agreed by an authorized representative of Optiver IP B.V. in 99 | writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 100 | CONDITIONS OF ANY KIND, either express or implied. Please see the License for the specific language governing 101 | permissions and limitations under the License. 102 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | mkdir build 4 | cd build 5 | 6 | supported_versions=( 11 12 ) 7 | for version in "${supported_versions[@]}" 8 | do 9 | rm -rf * 10 | cmake .. -DCMAKE_BUILD_TYPE=RelWithDebInfo -DPG_CONFIG=/usr/pgsql-${version}/bin/pg_config 11 | make -j4 12 | cpack 13 | cp *rpm ../ 14 | done 15 | 16 | cp ../*rpm /mnt/releases/postgresql 17 | 18 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(CMAKE_C_FLAGS_DEBUG "-DUSE_ASSERT_CHECKING=1") 2 | 3 | if (UNIX) 4 | set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -L${PG_LIBDIR}") 5 | set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -L${PG_LIBDIR}") 6 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${PG_CFLAGS}") 7 | set(CMAKE_CPP_FLAGS "${CMAKE_CPP_FLAGS} ${PG_CPPFLAGS}") 8 | set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g") 9 | endif (UNIX) 10 | 11 | if (APPLE) 12 | set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -multiply_defined suppress") 13 | set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -multiply_defined suppress -Wl,-undefined,dynamic_lookup -Wl,-dead_strip_dylibs -bundle_loader ${PG_BINDIR}/postgres") 14 | elseif (WIN32) 15 | set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /MANIFEST:NO") 16 | set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} /MANIFEST:NO") 17 | endif (APPLE) 18 | 19 | include_directories (${CMAKE_CURRENT_BINARY_DIR} ${PG_INCLUDEDIR} ${PG_INCLUDEDIR_SERVER}) 20 | 21 | if (WIN32) 22 | set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} ${PG_LIBDIR}/postgres.lib ws2_32.lib") 23 | set(CMAKE_C_FLAGS "-D_CRT_SECURE_NO_WARNINGS") 24 | include_directories(${PG_INCLUDEDIR_SERVER}/port/win32) 25 | 26 | if (MSVC) 27 | include_directories(${PG_INCLUDEDIR_SERVER}/port/win32_msvc) 28 | endif (MSVC) 29 | endif (WIN32) 30 | 31 | set(HEADERS 32 | timestamp9.h) 33 | 34 | set(SOURCES 35 | timestamp9.c) 36 | 37 | add_library(${PROJECT_NAME} MODULE ${SOURCES} ${HEADERS}) 38 | 39 | set_target_properties(${PROJECT_NAME} PROPERTIES 40 | OUTPUT_NAME ${PROJECT_NAME} 41 | PREFIX "") 42 | 43 | # Function to concatenate all files in SRC_FILE_LIST into file OUTPUT_FILE 44 | function(cat_files SRC_FILE_LIST OUTPUT_FILE) 45 | if (WIN32) 46 | # Make list of files into string of files separated by "+" 47 | # to make Windows copy concatenate them 48 | 49 | file(TO_NATIVE_PATH "${SRC_FILE_LIST}" SRC_FILE_LIST_NATIVE) 50 | string(REPLACE ";" ";+" SQL_LIST_JOINED "${SRC_FILE_LIST_NATIVE}") 51 | 52 | file(TO_NATIVE_PATH "${OUTPUT_FILE}" OUTPUT_FILE_NATIVE) 53 | 54 | set(CAT_CMD copy /B /y ${SQL_LIST_JOINED} "\"${OUTPUT_FILE_NATIVE}\"" >NUL) 55 | else () 56 | set(CAT_CMD cat ${SRC_FILE_LIST} > ${OUTPUT_FILE}) 57 | endif () 58 | add_custom_command( 59 | OUTPUT ${OUTPUT_FILE} 60 | DEPENDS ${SRC_FILE_LIST} 61 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 62 | COMMAND ${CAT_CMD} 63 | COMMENT "Generating ${OUTPUT_FILE}" 64 | ) 65 | endfunction() 66 | 67 | 68 | set(INSTALL_FILE ${PROJECT_NAME}--${PROJECT_VERSION_MOD}.sql) 69 | cat_files("timestamp9.sql" ${CMAKE_CURRENT_BINARY_DIR}/${INSTALL_FILE}) 70 | add_custom_target(sqlfile ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/${INSTALL_FILE}) 71 | 72 | set(MOD_FILES 73 | timestamp9--0.1.0--0.2.0.sql 74 | timestamp9--0.2.0--0.3.0.sql 75 | timestamp9--0.3.0--1.0.0.sql 76 | timestamp9--1.0.0--1.0.1.sql 77 | timestamp9--1.0.1--1.1.0.sql 78 | timestamp9--1.1.0--1.2.0.sql 79 | timestamp9--1.2.0--1.3.0.sql 80 | timestamp9--1.3.0--1.4.0.sql 81 | ) 82 | 83 | add_custom_target(sqlupdatescripts ALL DEPENDS ${MOD_FILES}) 84 | 85 | install( 86 | TARGETS ${PROJECT_NAME} 87 | DESTINATION ${PG_PKGLIBDIR}) 88 | 89 | install( 90 | FILES ${CMAKE_CURRENT_BINARY_DIR}/${INSTALL_FILE} 91 | ${MOD_FILES} 92 | DESTINATION "${PG_SHAREDIR}/extension") 93 | -------------------------------------------------------------------------------- /src/timestamp9--0.1.0--0.2.0.sql: -------------------------------------------------------------------------------- 1 | 2 | CREATE OR REPLACE FUNCTION timestamp9_to_date(timestamp9) RETURNS date AS 3 | '$libdir/timestamp9' 4 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 5 | 6 | CREATE CAST (timestamp9 AS date) 7 | WITH FUNCTION timestamp9_to_date(timestamp9) AS ASSIGNMENT; 8 | 9 | CREATE OR REPLACE FUNCTION date_to_timestamp9(date) RETURNS timestamp9 AS 10 | '$libdir/timestamp9' 11 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 12 | 13 | CREATE CAST (date AS timestamp9) 14 | WITH FUNCTION date_to_timestamp9(date) AS IMPLICIT; 15 | 16 | CREATE OR REPLACE FUNCTION hash_timestamp9(timestamp9) RETURNS integer AS 17 | 'hashint8' 18 | LANGUAGE internal IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 19 | 20 | CREATE OPERATOR CLASS hash_timestamp9_ops 21 | FOR TYPE timestamp9 USING hash FAMILY integer_ops AS 22 | OPERATOR 1 =, 23 | FUNCTION 1 hash_timestamp9(timestamp9); 24 | -------------------------------------------------------------------------------- /src/timestamp9--0.2.0--0.3.0.sql: -------------------------------------------------------------------------------- 1 | 2 | CREATE FUNCTION timestamp9_larger(timestamp9, timestamp9) RETURNS timestamp9 AS 3 | '$libdir/timestamp9' 4 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 5 | 6 | CREATE AGGREGATE max(timestamp9) ( 7 | SFUNC = timestamp9_larger, 8 | STYPE = timestamp9, 9 | SORTOP = > 10 | ); 11 | 12 | CREATE FUNCTION timestamp9_smaller(timestamp9, timestamp9) RETURNS timestamp9 AS 13 | '$libdir/timestamp9' 14 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 15 | CREATE AGGREGATE min(timestamp9) ( 16 | SFUNC = timestamp9_smaller, 17 | STYPE = timestamp9, 18 | SORTOP = < 19 | ); 20 | 21 | CREATE FUNCTION timestamp9_interval_pl(timestamp9, interval) RETURNS timestamp9 AS 22 | '$libdir/timestamp9' 23 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 24 | CREATE OPERATOR + ( 25 | LEFTARG = timestamp9, 26 | RIGHTARG = interval, 27 | PROCEDURE = timestamp9_interval_pl, 28 | COMMUTATOR = + 29 | ); 30 | 31 | CREATE FUNCTION timestamp9_interval_mi(timestamp9, interval) RETURNS timestamp9 AS 32 | '$libdir/timestamp9' 33 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 34 | CREATE OPERATOR - ( 35 | LEFTARG = timestamp9, 36 | RIGHTARG = interval, 37 | PROCEDURE = timestamp9_interval_mi 38 | ); 39 | 40 | CREATE FUNCTION interval_timestamp9_pl(interval, timestamp9) RETURNS timestamp9 AS 41 | '$libdir/timestamp9' 42 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 43 | CREATE OPERATOR + ( 44 | LEFTARG = interval, 45 | RIGHTARG = timestamp9, 46 | PROCEDURE = interval_timestamp9_pl, 47 | COMMUTATOR = + 48 | ); 49 | -------------------------------------------------------------------------------- /src/timestamp9--0.3.0--1.0.0.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optiver/timestamp9/2d689f894fae0dfee2bae0e6b1b5b78f67e7d67c/src/timestamp9--0.3.0--1.0.0.sql -------------------------------------------------------------------------------- /src/timestamp9--1.0.0--1.0.1.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optiver/timestamp9/2d689f894fae0dfee2bae0e6b1b5b78f67e7d67c/src/timestamp9--1.0.0--1.0.1.sql -------------------------------------------------------------------------------- /src/timestamp9--1.0.1--1.1.0.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optiver/timestamp9/2d689f894fae0dfee2bae0e6b1b5b78f67e7d67c/src/timestamp9--1.0.1--1.1.0.sql -------------------------------------------------------------------------------- /src/timestamp9--1.1.0--1.2.0.sql: -------------------------------------------------------------------------------- 1 | DROP OPERATOR CLASS hash_timestamp9_ops USING hash; 2 | 3 | CREATE OPERATOR CLASS hash_timestamp9_ops 4 | DEFAULT FOR TYPE timestamp9 USING hash FAMILY integer_ops AS 5 | OPERATOR 1 =, 6 | FUNCTION 1 hash_timestamp9(timestamp9); 7 | -------------------------------------------------------------------------------- /src/timestamp9--1.2.0--1.3.0.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optiver/timestamp9/2d689f894fae0dfee2bae0e6b1b5b78f67e7d67c/src/timestamp9--1.2.0--1.3.0.sql -------------------------------------------------------------------------------- /src/timestamp9--1.3.0--1.4.0.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optiver/timestamp9/2d689f894fae0dfee2bae0e6b1b5b78f67e7d67c/src/timestamp9--1.3.0--1.4.0.sql -------------------------------------------------------------------------------- /src/timestamp9.c: -------------------------------------------------------------------------------- 1 | 2 | #include "postgres.h" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "access/hash.h" 9 | #include "catalog/pg_type.h" 10 | #include "libpq/pqformat.h" 11 | #include "utils/array.h" 12 | #include "utils/builtins.h" 13 | #include "utils/timestamp.h" 14 | #include "utils/date.h" 15 | #include "utils/datetime.h" 16 | #include "parser/scansup.h" 17 | #include "pgtime.h" 18 | 19 | #include "timestamp9.h" 20 | 21 | PG_MODULE_MAGIC; 22 | 23 | PG_FUNCTION_INFO_V1(timestamp9_in); 24 | PG_FUNCTION_INFO_V1(timestamp9_out); 25 | PG_FUNCTION_INFO_V1(timestamp9_recv); 26 | PG_FUNCTION_INFO_V1(timestamp9_send); 27 | 28 | PG_FUNCTION_INFO_V1(timestamp9_eq); 29 | PG_FUNCTION_INFO_V1(timestamp9_ne); 30 | PG_FUNCTION_INFO_V1(timestamp9_lt); 31 | PG_FUNCTION_INFO_V1(timestamp9_le); 32 | PG_FUNCTION_INFO_V1(timestamp9_gt); 33 | PG_FUNCTION_INFO_V1(timestamp9_ge); 34 | 35 | PG_FUNCTION_INFO_V1(bt_timestamp9_cmp); 36 | 37 | PG_FUNCTION_INFO_V1(timestamp9_to_timestamptz); 38 | PG_FUNCTION_INFO_V1(timestamptz_to_timestamp9); 39 | PG_FUNCTION_INFO_V1(timestamp9_to_timestamp); 40 | PG_FUNCTION_INFO_V1(timestamp_to_timestamp9); 41 | PG_FUNCTION_INFO_V1(timestamp9_to_date); 42 | PG_FUNCTION_INFO_V1(date_to_timestamp9); 43 | 44 | PG_FUNCTION_INFO_V1(timestamp9_larger); 45 | PG_FUNCTION_INFO_V1(timestamp9_smaller); 46 | PG_FUNCTION_INFO_V1(timestamp9_interval_pl); 47 | PG_FUNCTION_INFO_V1(interval_timestamp9_pl); 48 | PG_FUNCTION_INFO_V1(timestamp9_interval_mi); 49 | 50 | #define kT_ns_in_s (int64_t)1000000000 51 | #define kT_ns_in_us (int64_t)1000 52 | 53 | #define NO_COLON_TZ_OFFSET_LEN (size_t)4 /* length of string 0200 */ 54 | #define COLON_TZ_OFFSET_LEN (size_t)5 /* length of string 02:00 */ 55 | 56 | static TimestampTz 57 | timestamp9_to_timestamptz_internal(timestamp9 ts9) 58 | { 59 | int64 us = ts9 / 1000; 60 | us -= ((POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * SECS_PER_DAY * USECS_PER_SEC); 61 | 62 | /* Recheck in case roundoff produces something just out of range */ 63 | if (!IS_VALID_TIMESTAMP(us)) 64 | ereport(ERROR, 65 | (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), 66 | errmsg("timestamp9 out of range: \"%lld\"", 67 | ts9))); 68 | return us; 69 | } 70 | 71 | static timestamp9 72 | timestamptz_to_timestamp9_internal(TimestampTz ts) 73 | { 74 | int64 ns; 75 | ts += ((POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * SECS_PER_DAY * USECS_PER_SEC); 76 | ns = ts * 1000; 77 | return ns; 78 | } 79 | 80 | static timestamp9 81 | date2timestamp9(DateADT dateVal) 82 | { 83 | timestamp9 result; 84 | struct pg_tm tt; 85 | struct pg_tm* tm = &tt; 86 | int tz; 87 | 88 | if (DATE_IS_NOBEGIN(dateVal) || DATE_IS_NOEND(dateVal)) 89 | ereport(ERROR, 90 | (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), 91 | errmsg("date out of range for timestamp"))); 92 | else 93 | { 94 | /* 95 | * Date's range is wider than timestamp's, so check for boundaries. 96 | * Since dates have the same minimum values as timestamps, only upper 97 | * boundary need be checked for overflow. 98 | */ 99 | if (dateVal >= (TIMESTAMP9_END_JULIAN - UNIX_EPOCH_JDATE)) 100 | ereport(ERROR, 101 | (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), 102 | errmsg("date out of range for timestamp"))); 103 | 104 | j2date(dateVal + POSTGRES_EPOCH_JDATE, 105 | &(tm->tm_year), &(tm->tm_mon), &(tm->tm_mday)); 106 | tm->tm_hour = 0; 107 | tm->tm_min = 0; 108 | tm->tm_sec = 0; 109 | tz = DetermineTimeZoneOffset(tm, session_timezone); 110 | 111 | result = dateVal * USECS_PER_DAY * kT_ns_in_us + tz * USECS_PER_SEC * kT_ns_in_us + 112 | (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * USECS_PER_DAY * kT_ns_in_us; 113 | } 114 | 115 | return result; 116 | } 117 | 118 | static TimestampTz 119 | timestamp2timestamptz(Timestamp timestamp) 120 | { 121 | TimestampTz result; 122 | struct pg_tm tt; 123 | struct pg_tm* tm = &tt; 124 | fsec_t fsec; 125 | int tz; 126 | 127 | if (TIMESTAMP_NOT_FINITE(timestamp)) 128 | result = timestamp; 129 | else 130 | { 131 | if (timestamp2tm(timestamp, NULL, tm, &fsec, NULL, NULL) != 0) 132 | ereport(ERROR, 133 | (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), 134 | errmsg("timestamp out of range"))); 135 | 136 | tz = DetermineTimeZoneOffset(tm, session_timezone); 137 | 138 | if (tm2timestamp(tm, fsec, &tz, &result) != 0) 139 | ereport(ERROR, 140 | (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), 141 | errmsg("timestamp out of range"))); 142 | } 143 | 144 | return result; 145 | } 146 | 147 | static Timestamp 148 | timestamptz2timestamp(TimestampTz timestamp) 149 | { 150 | Timestamp result; 151 | struct pg_tm tt; 152 | struct pg_tm* tm = &tt; 153 | fsec_t fsec; 154 | int tz; 155 | 156 | if (TIMESTAMP_NOT_FINITE(timestamp)) 157 | result = timestamp; 158 | else 159 | { 160 | if (timestamp2tm(timestamp, &tz, tm, &fsec, NULL, NULL) != 0) 161 | ereport(ERROR, 162 | (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), 163 | errmsg("timestamp out of range"))); 164 | if (tm2timestamp(tm, fsec, NULL, &result) != 0) 165 | ereport(ERROR, 166 | (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), 167 | errmsg("timestamp out of range"))); 168 | } 169 | return result; 170 | } 171 | 172 | /* 173 | * This function is directly copied from backend/utils/adt/timestamp.c. 174 | * Additionally, we check if the numeric time zone contains a valid numeric 175 | * part, since DecodeTimezone doesn't check it. 176 | */ 177 | static int 178 | parse_sane_timezone(struct pg_tm *tm, char *tzname) 179 | #if PG16_GE 180 | { 181 | int dterr; 182 | int tz; 183 | 184 | /* 185 | * Look up the requested timezone. First we try to interpret it as a 186 | * numeric timezone specification; if DecodeTimezone decides it doesn't 187 | * like the format, we try timezone abbreviations and names. 188 | * 189 | * Note pg_tzset happily parses numeric input that DecodeTimezone would 190 | * reject. To avoid having it accept input that would otherwise be seen 191 | * as invalid, it's enough to disallow having a digit in the first 192 | * position of our input string. 193 | */ 194 | if (isdigit((unsigned char) *tzname)) 195 | ereport(ERROR, 196 | (errcode(ERRCODE_INVALID_PARAMETER_VALUE), 197 | errmsg("invalid input syntax for type %s: \"%s\"", 198 | "numeric time zone", tzname), 199 | errhint("Numeric time zones must have \"-\" or \"+\" as first character."))); 200 | 201 | /* 202 | * If tzname is a numeric time zone, let's check if it contains a valid numeric part. 203 | */ 204 | if ((tzname[0] == '+' || tzname[0] == '-') && tzname[1] == '\0') 205 | { 206 | ereport(ERROR, 207 | (errcode(ERRCODE_INVALID_PARAMETER_VALUE), 208 | errmsg("invalid input syntax for type %s: \"%s\"", "numeric timezone", tzname), 209 | errhint("Numeric time zones must have the numeric part"))); 210 | } 211 | 212 | dterr = DecodeTimezone(tzname, &tz); 213 | if (dterr != 0) 214 | { 215 | int type, 216 | val; 217 | pg_tz *tzp; 218 | 219 | if (dterr == DTERR_TZDISP_OVERFLOW) 220 | ereport(ERROR, 221 | (errcode(ERRCODE_INVALID_PARAMETER_VALUE), 222 | errmsg("numeric time zone \"%s\" out of range", tzname))); 223 | else if (dterr != DTERR_BAD_FORMAT) 224 | ereport(ERROR, 225 | (errcode(ERRCODE_INVALID_PARAMETER_VALUE), 226 | errmsg("time zone \"%s\" not recognized", tzname))); 227 | 228 | type = DecodeTimezoneName(tzname, &val, &tzp); 229 | 230 | if (type == TZNAME_FIXED_OFFSET) 231 | { 232 | /* fixed-offset abbreviation */ 233 | tz = -val; 234 | } 235 | else if (type == TZNAME_DYNTZ) 236 | { 237 | /* dynamic-offset abbreviation, resolve using specified time */ 238 | tz = DetermineTimeZoneAbbrevOffset(tm, tzname, tzp); 239 | } 240 | else 241 | { 242 | /* full zone name */ 243 | tz = DetermineTimeZoneOffset(tm, tzp); 244 | } 245 | } 246 | 247 | return tz; 248 | } 249 | #else 250 | { 251 | int rt; 252 | int tz; 253 | 254 | /* 255 | * Look up the requested timezone. First we try to interpret it as a 256 | * numeric timezone specification; if DecodeTimezone decides it doesn't 257 | * like the format, we look in the timezone abbreviation table (to handle 258 | * cases like "EST"), and if that also fails, we look in the timezone 259 | * database (to handle cases like "America/New_York"). (This matches the 260 | * order in which timestamp input checks the cases; it's important because 261 | * the timezone database unwisely uses a few zone names that are identical 262 | * to offset abbreviations.) 263 | * 264 | * Note pg_tzset happily parses numeric input that DecodeTimezone would 265 | * reject. To avoid having it accept input that would otherwise be seen 266 | * as invalid, it's enough to disallow having a digit in the first 267 | * position of our input string. 268 | */ 269 | if (isdigit((unsigned char) *tzname)) 270 | ereport(ERROR, 271 | (errcode(ERRCODE_INVALID_PARAMETER_VALUE), 272 | errmsg("invalid input syntax for type %s: \"%s\"", 273 | "numeric time zone", tzname), 274 | errhint("Numeric time zones must have \"-\" or \"+\" as first character."))); 275 | 276 | /* 277 | * If tzname is a numeric time zone, let's check if it contains a valid numeric part. 278 | */ 279 | if ((tzname[0] == '+' || tzname[0] == '-') && tzname[1] == '\0') 280 | { 281 | ereport(ERROR, 282 | (errcode(ERRCODE_INVALID_PARAMETER_VALUE), 283 | errmsg("invalid input syntax for type %s: \"%s\"", "numeric timezone", tzname), 284 | errhint("Numeric time zones must have the numeric part"))); 285 | } 286 | 287 | rt = DecodeTimezone(tzname, &tz); 288 | if (rt != 0) 289 | { 290 | char *lowzone; 291 | int type, 292 | val; 293 | pg_tz *tzp; 294 | 295 | if (rt == DTERR_TZDISP_OVERFLOW) 296 | ereport(ERROR, 297 | (errcode(ERRCODE_INVALID_PARAMETER_VALUE), 298 | errmsg("numeric time zone \"%s\" out of range", tzname))); 299 | else if (rt != DTERR_BAD_FORMAT) 300 | ereport(ERROR, 301 | (errcode(ERRCODE_INVALID_PARAMETER_VALUE), 302 | errmsg("time zone \"%s\" not recognized", tzname))); 303 | 304 | /* DecodeTimezoneAbbrev requires lowercase input */ 305 | lowzone = downcase_truncate_identifier(tzname, 306 | strlen(tzname), 307 | false); 308 | type = DecodeTimezoneAbbrev(0, lowzone, &val, &tzp); 309 | 310 | if (type == TZ || type == DTZ) 311 | { 312 | /* fixed-offset abbreviation */ 313 | tz = -val; 314 | } 315 | else if (type == DYNTZ) 316 | { 317 | /* dynamic-offset abbreviation, resolve using specified time */ 318 | tz = DetermineTimeZoneAbbrevOffset(tm, tzname, tzp); 319 | } 320 | else 321 | { 322 | /* try it as a full zone name */ 323 | tzp = pg_tzset(tzname); 324 | if (tzp) 325 | tz = DetermineTimeZoneOffset(tm, tzp); 326 | else 327 | ereport(ERROR, 328 | (errcode(ERRCODE_INVALID_PARAMETER_VALUE), 329 | errmsg("time zone \"%s\" not recognized", tzname))); 330 | } 331 | } 332 | 333 | return tz; 334 | } 335 | #endif 336 | 337 | /* 338 | * timestamp9_in - converts "num" to timestamp9 339 | */ 340 | Datum 341 | timestamp9_in(PG_FUNCTION_ARGS) 342 | { 343 | char *str = PG_GETARG_CSTRING(0); 344 | 345 | timestamp9 result = 0ll; 346 | fsec_t fsec; 347 | struct pg_tm tt; 348 | struct pg_tm* p_tm = &tt; 349 | int dtype; 350 | int tz; 351 | int nf; 352 | char *field[MAXDATEFIELDS]; 353 | int ftype[MAXDATEFIELDS]; 354 | char lowstr[MAXDATELEN + MAXDATEFIELDS]; 355 | long long ratio; 356 | bool fractional_valid = false; 357 | size_t len = strlen(str); 358 | int parsed_length; 359 | long long ns; 360 | #if PG16_GE 361 | DateTimeErrorExtra extra; 362 | #endif 363 | 364 | if (len > MAXDATELEN) 365 | { 366 | ereport(ERROR, 367 | (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), 368 | errmsg("input string too long. invalid input format for timestamp9" 369 | ))); 370 | } 371 | 372 | /* first try the raw nanosecond bigint format: eg. 1554809728000100000 */ 373 | if(sscanf(str, "%lld%n", &ns, &parsed_length) == 1) 374 | { 375 | if ((size_t)parsed_length == len) 376 | { 377 | PG_RETURN_TIMESTAMP9(ns); 378 | } 379 | } 380 | 381 | ratio = parse_fractional_ratio(str, len, &fractional_valid); 382 | 383 | /* then try postgres parsing of up-to microsecond fractional second timestamp (to allow greater flexibility in input) */ 384 | if (ratio <= 100 || 385 | ParseDateTime(str, lowstr, MAXDATELEN + MAXDATEFIELDS, field, ftype, MAXDATEFIELDS, &nf) != 0 || 386 | #if PG16_GE 387 | DecodeDateTime(field, ftype, nf, &dtype, p_tm, &fsec, &tz, &extra) != 0) 388 | #else 389 | DecodeDateTime(field, ftype, nf, &dtype, p_tm, &fsec, &tz) != 0) 390 | #endif 391 | { 392 | /* it doesn't work - try our own simple parsing then */ 393 | struct tm tm_ = {0}; 394 | char gmt_offset_str[TZ_STRLEN_MAX + 1] = ""; 395 | int gmt_offset = 0; 396 | int num_read; 397 | time_t tt; 398 | 399 | num_read = sscanf(str, "%d-%d-%d %d:%d:%d.%lld %255s", &tm_.tm_year, &tm_.tm_mon, &tm_.tm_mday, &tm_.tm_hour, &tm_.tm_min, &tm_.tm_sec, &ns, gmt_offset_str); 400 | if ((num_read == 7 || num_read == 8) && fractional_valid) 401 | { 402 | int gmt_offset_str_len = strlen(gmt_offset_str); 403 | struct pg_tm temp_tm = {0}; 404 | temp_tm.tm_year = tm_.tm_year; 405 | temp_tm.tm_mon = tm_.tm_mon; 406 | temp_tm.tm_mday = tm_.tm_mday; 407 | if (gmt_offset_str_len != 0) 408 | { 409 | /* If we have specified the timezone, try to decode it. */ 410 | gmt_offset = parse_sane_timezone(&temp_tm, gmt_offset_str); 411 | } 412 | else 413 | { 414 | /* If we haven't specified the timezone, let's use session_timezone to determin the gmt_offset. */ 415 | gmt_offset = DetermineTimeZoneOffset(&temp_tm, session_timezone); 416 | } 417 | 418 | tm_.tm_year -= 1900; 419 | tm_.tm_mon--; 420 | tt = timegm(&tm_); 421 | tt = tt + tm_.tm_gmtoff + gmt_offset; 422 | 423 | result = (long long)tt * kT_ns_in_s + (ns * ratio); 424 | } 425 | else 426 | { 427 | ereport(ERROR, 428 | (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), 429 | errmsg("invalid input format for timestamp9, required format y-m-d h:m:s.ns [+tz] \"%s\"", 430 | str))); 431 | } 432 | PG_RETURN_TIMESTAMP9(result); 433 | } 434 | 435 | switch (dtype) 436 | { 437 | case DTK_DATE: 438 | { 439 | Timestamp pg_ts; 440 | if (tm2timestamp(p_tm, fsec, &tz, &pg_ts) != 0) 441 | { 442 | ereport(ERROR, 443 | (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), 444 | errmsg("invalid input format for timestamp9, required format y-m-d h:m:s.ns [+tz] \"%s\"", 445 | str))); 446 | } 447 | pg_ts += ((POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * SECS_PER_DAY * USECS_PER_SEC); 448 | result = pg_ts * 1000; 449 | break; 450 | } 451 | default: 452 | ereport(ERROR, 453 | (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), 454 | errmsg("invalid input format for timestamp9, required format y-m-d h:m:s.ns [+tz] \"%s\"", 455 | str))); 456 | } 457 | 458 | PG_RETURN_TIMESTAMP9(result); 459 | } 460 | 461 | long long parse_fractional_ratio(const char* str, size_t len, bool* fractional_valid) 462 | { 463 | bool count = false; 464 | long long ratio = 1000000000ll; 465 | size_t i = 0; 466 | *fractional_valid = false; 467 | 468 | while (i <= len) 469 | { 470 | if (count && (str[i] == ' ' || str[i] == '+' || str[i] == '-' || str[i] == 'Z' || str[i] == '\0')) 471 | { 472 | *fractional_valid = (ratio > 0); 473 | break; 474 | } 475 | 476 | if (count) 477 | ratio /= 10; 478 | 479 | if (str[i] == '.') 480 | count = true; 481 | i++; 482 | } 483 | return ratio; 484 | } 485 | 486 | /* 487 | * timestamp9_out - converts timestamp9 to "num" 488 | */ 489 | Datum 490 | timestamp9_out(PG_FUNCTION_ARGS) 491 | { 492 | timestamp9 arg1 = PG_GETARG_TIMESTAMP9(0); 493 | char *result = (char *) palloc(41); 494 | time_t secs = (time_t)(arg1 / kT_ns_in_s); 495 | struct pg_tm *tm_; 496 | size_t offset; 497 | long long int mod = (arg1 % kT_ns_in_s); 498 | if (mod < 0) 499 | { 500 | mod += kT_ns_in_s; 501 | secs -= 1; 502 | } 503 | 504 | tm_ = pg_localtime(&secs, session_timezone); 505 | if (!tm_) 506 | ereport(ERROR, 507 | (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), 508 | errmsg("timestamp9 out of range"))); 509 | 510 | offset = pg_strftime(result, 41, "%Y-%m-%d %H:%M:%S", tm_); 511 | offset += sprintf(result + offset, ".%09lld", mod); 512 | offset += pg_strftime(result + offset, 41, " %z", tm_); 513 | 514 | PG_RETURN_CSTRING(result); 515 | } 516 | 517 | /* 518 | * timestamp9_recv - converts external binary format to timestamp9 519 | */ 520 | Datum 521 | timestamp9_recv(PG_FUNCTION_ARGS) 522 | { 523 | StringInfo buf = (StringInfo) PG_GETARG_POINTER(0); 524 | 525 | PG_RETURN_TIMESTAMP9((timestamp9) pq_getmsgint64(buf)); 526 | } 527 | 528 | /* 529 | * timestamp9_send - converts timestamp9 to binary format 530 | */ 531 | Datum 532 | timestamp9_send(PG_FUNCTION_ARGS) 533 | { 534 | timestamp9 arg1 = PG_GETARG_TIMESTAMP9(0); 535 | StringInfoData buf; 536 | 537 | pq_begintypsend(&buf); 538 | pq_sendint64(&buf, arg1); 539 | PG_RETURN_BYTEA_P(pq_endtypsend(&buf)); 540 | } 541 | 542 | 543 | Datum 544 | timestamp9_eq(PG_FUNCTION_ARGS) 545 | { 546 | PG_RETURN_BOOL(PG_GETARG_TIMESTAMP9(0) == PG_GETARG_TIMESTAMP9(1)); 547 | } 548 | 549 | Datum 550 | timestamp9_ne(PG_FUNCTION_ARGS) 551 | { 552 | PG_RETURN_BOOL(PG_GETARG_TIMESTAMP9(0) != PG_GETARG_TIMESTAMP9(1)); 553 | } 554 | 555 | Datum 556 | timestamp9_lt(PG_FUNCTION_ARGS) 557 | { 558 | PG_RETURN_BOOL(PG_GETARG_TIMESTAMP9(0) < PG_GETARG_TIMESTAMP9(1)); 559 | } 560 | 561 | Datum 562 | timestamp9_le(PG_FUNCTION_ARGS) 563 | { 564 | PG_RETURN_BOOL(PG_GETARG_TIMESTAMP9(0) <= PG_GETARG_TIMESTAMP9(1)); 565 | } 566 | 567 | Datum 568 | timestamp9_gt(PG_FUNCTION_ARGS) 569 | { 570 | PG_RETURN_BOOL(PG_GETARG_TIMESTAMP9(0) > PG_GETARG_TIMESTAMP9(1)); 571 | } 572 | 573 | Datum 574 | timestamp9_ge(PG_FUNCTION_ARGS) 575 | { 576 | PG_RETURN_BOOL(PG_GETARG_TIMESTAMP9(0) >= PG_GETARG_TIMESTAMP9(1)); 577 | } 578 | 579 | Datum 580 | bt_timestamp9_cmp(PG_FUNCTION_ARGS) 581 | { 582 | timestamp9 a = PG_GETARG_TIMESTAMP9(0); 583 | timestamp9 b = PG_GETARG_TIMESTAMP9(1); 584 | 585 | PG_RETURN_INT32((a < b) ? -1 : (a > b)); 586 | } 587 | 588 | Datum timestamp9_to_timestamptz(PG_FUNCTION_ARGS) 589 | { 590 | timestamp9 ts9 = PG_GETARG_TIMESTAMP9(0); 591 | TimestampTz us = timestamp9_to_timestamptz_internal(ts9); 592 | PG_RETURN_TIMESTAMPTZ(us); 593 | } 594 | 595 | Datum timestamptz_to_timestamp9(PG_FUNCTION_ARGS) 596 | { 597 | TimestampTz ts = PG_GETARG_TIMESTAMPTZ(0); 598 | timestamp9 ns = timestamptz_to_timestamp9_internal(ts); 599 | 600 | PG_RETURN_TIMESTAMP9(ns); 601 | } 602 | 603 | Datum timestamp9_to_timestamp(PG_FUNCTION_ARGS) 604 | { 605 | timestamp9 ts9 = PG_GETARG_TIMESTAMP9(0); 606 | TimestampTz us = ts9 / 1000; 607 | us -= ((POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * SECS_PER_DAY * USECS_PER_SEC); 608 | 609 | /* Recheck in case roundoff produces something just out of range */ 610 | if (!IS_VALID_TIMESTAMP(us)) 611 | ereport(ERROR, 612 | (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), 613 | errmsg("timestamp9 out of range: \"%lld\"", 614 | PG_GETARG_TIMESTAMP9(0)))); 615 | 616 | us = timestamptz2timestamp(us); 617 | PG_RETURN_TIMESTAMP(us); 618 | } 619 | 620 | Datum timestamp_to_timestamp9(PG_FUNCTION_ARGS) 621 | { 622 | timestamp9 ns; 623 | TimestampTz ts = PG_GETARG_TIMESTAMP(0); 624 | ts = timestamp2timestamptz(ts); 625 | ts += ((POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * SECS_PER_DAY * USECS_PER_SEC); 626 | ns = ts * 1000; 627 | 628 | PG_RETURN_TIMESTAMP9(ns); 629 | } 630 | 631 | Datum timestamp9_to_date(PG_FUNCTION_ARGS) 632 | { 633 | timestamp9 ts9 = PG_GETARG_TIMESTAMP9(0); 634 | TimestampTz timestamp = timestamp9_to_timestamptz_internal(ts9); 635 | DateADT result; 636 | struct pg_tm tt, 637 | *tm = &tt; 638 | fsec_t fsec; 639 | int tz; 640 | 641 | if (timestamp2tm(timestamp, &tz, tm, &fsec, NULL, NULL) != 0) 642 | ereport(ERROR, 643 | (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), 644 | errmsg("timestamp out of range"))); 645 | 646 | result = date2j(tm->tm_year, tm->tm_mon, tm->tm_mday) - POSTGRES_EPOCH_JDATE; 647 | 648 | PG_RETURN_DATEADT(result); 649 | } 650 | 651 | 652 | Datum date_to_timestamp9(PG_FUNCTION_ARGS) 653 | { 654 | DateADT date = PG_GETARG_DATEADT(0); 655 | timestamp9 ts = date2timestamp9(date); 656 | PG_RETURN_TIMESTAMP9(ts); 657 | } 658 | 659 | Datum timestamp9_larger(PG_FUNCTION_ARGS) 660 | { 661 | timestamp9 a = PG_GETARG_TIMESTAMP9(0); 662 | timestamp9 b = PG_GETARG_TIMESTAMP9(1); 663 | if (a > b) 664 | PG_RETURN_TIMESTAMP9(a); 665 | else 666 | PG_RETURN_TIMESTAMP9(b); 667 | } 668 | 669 | Datum timestamp9_smaller(PG_FUNCTION_ARGS) 670 | { 671 | timestamp9 a = PG_GETARG_TIMESTAMP9(0); 672 | timestamp9 b = PG_GETARG_TIMESTAMP9(1); 673 | if (a < b) 674 | PG_RETURN_TIMESTAMP9(a); 675 | else 676 | PG_RETURN_TIMESTAMP9(b); 677 | } 678 | 679 | Datum timestamp9_interval_pl(PG_FUNCTION_ARGS) 680 | { 681 | timestamp9 ts = PG_GETARG_TIMESTAMP9(0); 682 | Interval* intvl = PG_GETARG_INTERVAL_P(1); 683 | 684 | TimestampTz tstz = timestamp9_to_timestamptz_internal(ts); 685 | timestamp9 new_ts = timestamptz_to_timestamp9_internal( 686 | DatumGetTimestampTz( 687 | DirectFunctionCall2(timestamptz_pl_interval, 688 | TimestampTzGetDatum(tstz), 689 | IntervalPGetDatum(intvl)))); 690 | new_ts += ts % 1000; 691 | PG_RETURN_TIMESTAMP9(new_ts); 692 | } 693 | 694 | Datum interval_timestamp9_pl(PG_FUNCTION_ARGS) 695 | { 696 | Interval* intvl = PG_GETARG_INTERVAL_P(0); 697 | timestamp9 ts = PG_GETARG_TIMESTAMP9(1); 698 | 699 | TimestampTz tstz = timestamp9_to_timestamptz_internal(ts); 700 | timestamp9 new_ts = timestamptz_to_timestamp9_internal( 701 | DatumGetTimestampTz( 702 | DirectFunctionCall2(timestamptz_pl_interval, 703 | TimestampTzGetDatum(tstz), 704 | IntervalPGetDatum(intvl)))); 705 | new_ts += ts % 1000; 706 | PG_RETURN_TIMESTAMP9(new_ts); 707 | } 708 | 709 | Datum timestamp9_interval_mi(PG_FUNCTION_ARGS) 710 | { 711 | timestamp9 ts = PG_GETARG_TIMESTAMP9(0); 712 | Interval* intvl = PG_GETARG_INTERVAL_P(1); 713 | Interval tspan; 714 | TimestampTz tstz; 715 | timestamp9 new_ts; 716 | 717 | tspan.month = -intvl->month; 718 | tspan.day = -intvl->day; 719 | tspan.time = -intvl->time; 720 | 721 | tstz = timestamp9_to_timestamptz_internal(ts); 722 | new_ts = timestamptz_to_timestamp9_internal(DatumGetTimestampTz(DirectFunctionCall2(timestamptz_pl_interval, 723 | TimestampTzGetDatum(tstz), 724 | IntervalPGetDatum(&tspan)))); 725 | new_ts += ts % 1000; 726 | PG_RETURN_TIMESTAMP9(new_ts); 727 | } 728 | -------------------------------------------------------------------------------- /src/timestamp9.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include 5 | #include "fmgr.h" 6 | #include "postgres.h" 7 | 8 | #define PG16_GE (PG_VERSION_NUM >= 160000) 9 | 10 | typedef long long timestamp9; 11 | 12 | #define DatumGetTimestamp9(X) ((timestamp9) (X)) 13 | #define Timestamp9GetDatum(X) ((Datum) (X)) 14 | 15 | #define PG_GETARG_TIMESTAMP9(n) DatumGetTimestamp9(PG_GETARG_DATUM(n)) 16 | #define PG_RETURN_TIMESTAMP9(x) return Timestamp9GetDatum(x) 17 | 18 | #define TIMESTAMP9_END_JULIAN (2547238) /* == date2j(2262, 1, 1) */ 19 | 20 | extern Datum timestamp9_in(PG_FUNCTION_ARGS); 21 | extern Datum timestamp9_out(PG_FUNCTION_ARGS); 22 | extern Datum timestamp9_recv(PG_FUNCTION_ARGS); 23 | extern Datum timestamp9_send(PG_FUNCTION_ARGS); 24 | 25 | extern Datum timestamp9_eq(PG_FUNCTION_ARGS); 26 | extern Datum timestamp9_ne(PG_FUNCTION_ARGS); 27 | extern Datum timestamp9_lt(PG_FUNCTION_ARGS); 28 | extern Datum timestamp9_le(PG_FUNCTION_ARGS); 29 | extern Datum timestamp9_gt(PG_FUNCTION_ARGS); 30 | extern Datum timestamp9_ge(PG_FUNCTION_ARGS); 31 | 32 | extern Datum bt_timestamp9_cmp(PG_FUNCTION_ARGS); 33 | 34 | extern Datum timestamp9_to_timestamptz(PG_FUNCTION_ARGS); 35 | extern Datum timestamptz_to_timestamp9(PG_FUNCTION_ARGS); 36 | extern Datum timestamp9_to_timestamp(PG_FUNCTION_ARGS); 37 | extern Datum timestamp_to_timestamp9(PG_FUNCTION_ARGS); 38 | extern Datum timestamp9_to_date(PG_FUNCTION_ARGS); 39 | extern Datum date_to_timestamp9(PG_FUNCTION_ARGS); 40 | 41 | extern Datum timestamp9_larger(PG_FUNCTION_ARGS); 42 | extern Datum timestamp9_smaller(PG_FUNCTION_ARGS); 43 | extern Datum timestamp9_interval_pl(PG_FUNCTION_ARGS); 44 | extern Datum interval_timestamp9_pl(PG_FUNCTION_ARGS); 45 | extern Datum timestamp9_interval_mi(PG_FUNCTION_ARGS); 46 | 47 | int parse_gmt_offset(const char*, bool*); 48 | 49 | long long parse_fractional_ratio(const char*, size_t, bool*); 50 | -------------------------------------------------------------------------------- /src/timestamp9.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION timestamp9_in(cstring) RETURNS timestamp9 AS 2 | '$libdir/timestamp9' 3 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 4 | CREATE OR REPLACE FUNCTION timestamp9_out(timestamp9) RETURNS cstring AS 5 | '$libdir/timestamp9' 6 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 7 | CREATE OR REPLACE FUNCTION timestamp9_recv(internal) RETURNS timestamp9 AS 8 | '$libdir/timestamp9' 9 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 10 | CREATE OR REPLACE FUNCTION timestamp9_send(timestamp9) RETURNS bytea AS 11 | '$libdir/timestamp9' 12 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 13 | CREATE TYPE timestamp9 ( 14 | INPUT = timestamp9_in, 15 | OUTPUT = timestamp9_out, 16 | RECEIVE = timestamp9_recv, 17 | SEND = timestamp9_send, 18 | INTERNALLENGTH = 8, 19 | PASSEDBYVALUE, 20 | ALIGNMENT = double, 21 | STORAGE = plain 22 | ); 23 | 24 | CREATE OR REPLACE FUNCTION timestamp9_to_timestamptz(timestamp9) RETURNS timestamptz AS 25 | '$libdir/timestamp9' 26 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 27 | 28 | CREATE CAST (timestamp9 AS timestamptz) 29 | WITH FUNCTION timestamp9_to_timestamptz(timestamp9) AS ASSIGNMENT; 30 | 31 | CREATE OR REPLACE FUNCTION timestamptz_to_timestamp9(timestamptz) RETURNS timestamp9 AS 32 | '$libdir/timestamp9' 33 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 34 | 35 | CREATE CAST (timestamptz AS timestamp9) 36 | WITH FUNCTION timestamptz_to_timestamp9(timestamptz) AS IMPLICIT; 37 | 38 | 39 | CREATE OR REPLACE FUNCTION timestamp9_to_timestamp(timestamp9) RETURNS timestamp AS 40 | '$libdir/timestamp9' 41 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 42 | 43 | CREATE CAST (timestamp9 AS timestamp) 44 | WITH FUNCTION timestamp9_to_timestamp(timestamp9) AS ASSIGNMENT; 45 | 46 | CREATE OR REPLACE FUNCTION timestamp_to_timestamp9(timestamp) RETURNS timestamp9 AS 47 | '$libdir/timestamp9' 48 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 49 | 50 | CREATE CAST (timestamp AS timestamp9) 51 | WITH FUNCTION timestamp_to_timestamp9(timestamp) AS IMPLICIT; 52 | 53 | 54 | CREATE OR REPLACE FUNCTION timestamp9_to_date(timestamp9) RETURNS date AS 55 | '$libdir/timestamp9' 56 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 57 | 58 | CREATE CAST (timestamp9 AS date) 59 | WITH FUNCTION timestamp9_to_date(timestamp9) AS ASSIGNMENT; 60 | 61 | CREATE OR REPLACE FUNCTION date_to_timestamp9(date) RETURNS timestamp9 AS 62 | '$libdir/timestamp9' 63 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 64 | 65 | CREATE CAST (date AS timestamp9) 66 | WITH FUNCTION date_to_timestamp9(date) AS IMPLICIT; 67 | 68 | 69 | CREATE CAST (timestamp9 AS bigint) 70 | WITHOUT FUNCTION AS ASSIGNMENT; 71 | CREATE CAST (bigint AS timestamp9) 72 | WITHOUT FUNCTION AS IMPLICIT; 73 | 74 | CREATE OR REPLACE FUNCTION timestamp9_lt(timestamp9, timestamp9) RETURNS bool AS 75 | 'MODULE_PATHNAME' 76 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 77 | CREATE OPERATOR < ( 78 | LEFTARG = timestamp9, 79 | RIGHTARG = timestamp9, 80 | PROCEDURE = timestamp9_lt, 81 | COMMUTATOR = >, 82 | NEGATOR = >=, 83 | RESTRICT = scalarltsel, 84 | JOIN = scalarltjoinsel 85 | ); 86 | 87 | CREATE OR REPLACE FUNCTION timestamp9_le(timestamp9, timestamp9) RETURNS bool AS 88 | 'MODULE_PATHNAME' 89 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 90 | CREATE OPERATOR <= ( 91 | LEFTARG = timestamp9, 92 | RIGHTARG = timestamp9, 93 | PROCEDURE = timestamp9_le, 94 | COMMUTATOR = >=, 95 | NEGATOR = >, 96 | RESTRICT = scalarltsel, 97 | JOIN = scalarltjoinsel 98 | ); 99 | 100 | CREATE OR REPLACE FUNCTION timestamp9_gt(timestamp9, timestamp9) RETURNS bool AS 101 | 'MODULE_PATHNAME' 102 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 103 | CREATE OPERATOR > ( 104 | LEFTARG = timestamp9, 105 | RIGHTARG = timestamp9, 106 | PROCEDURE = timestamp9_gt, 107 | COMMUTATOR = <, 108 | NEGATOR = <=, 109 | RESTRICT = scalargtsel, 110 | JOIN = scalargtjoinsel 111 | ); 112 | 113 | CREATE OR REPLACE FUNCTION timestamp9_ge(timestamp9, timestamp9) RETURNS bool AS 114 | 'MODULE_PATHNAME' 115 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 116 | CREATE OPERATOR >= ( 117 | LEFTARG = timestamp9, 118 | RIGHTARG = timestamp9, 119 | PROCEDURE = timestamp9_ge, 120 | COMMUTATOR = <=, 121 | NEGATOR = <, 122 | RESTRICT = scalargtsel, 123 | JOIN = scalargtjoinsel 124 | ); 125 | 126 | CREATE OR REPLACE FUNCTION timestamp9_eq(timestamp9, timestamp9) RETURNS bool AS 127 | 'MODULE_PATHNAME' 128 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 129 | CREATE OPERATOR = ( 130 | LEFTARG = timestamp9, 131 | RIGHTARG = timestamp9, 132 | PROCEDURE = timestamp9_eq, 133 | COMMUTATOR = =, 134 | NEGATOR = <>, 135 | RESTRICT = eqsel, 136 | JOIN = eqjoinsel, 137 | HASHES, 138 | MERGES 139 | ); 140 | 141 | CREATE OR REPLACE FUNCTION timestamp9_ne(timestamp9, timestamp9) RETURNS bool AS 142 | 'MODULE_PATHNAME' 143 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 144 | CREATE OPERATOR <> ( 145 | LEFTARG = timestamp9, 146 | RIGHTARG = timestamp9, 147 | PROCEDURE = timestamp9_ne, 148 | COMMUTATOR = <>, 149 | NEGATOR = =, 150 | RESTRICT = neqsel, 151 | JOIN = neqjoinsel, 152 | MERGES 153 | ); 154 | 155 | CREATE OR REPLACE FUNCTION bt_timestamp9_cmp(timestamp9, timestamp9) RETURNS int AS 156 | 'MODULE_PATHNAME' 157 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 158 | 159 | 160 | CREATE OPERATOR CLASS btree_timestamp9_ops 161 | DEFAULT FOR TYPE timestamp9 USING btree FAMILY integer_ops AS 162 | OPERATOR 1 <, 163 | OPERATOR 2 <=, 164 | OPERATOR 3 =, 165 | OPERATOR 4 >=, 166 | OPERATOR 5 >, 167 | FUNCTION 1 bt_timestamp9_cmp(timestamp9, timestamp9); 168 | 169 | 170 | CREATE OR REPLACE FUNCTION hash_timestamp9(timestamp9) RETURNS integer AS 171 | 'hashint8' 172 | LANGUAGE internal IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 173 | 174 | CREATE OPERATOR CLASS hash_timestamp9_ops 175 | DEFAULT FOR TYPE timestamp9 USING hash FAMILY integer_ops AS 176 | OPERATOR 1 =, 177 | FUNCTION 1 hash_timestamp9(timestamp9); 178 | 179 | 180 | CREATE FUNCTION timestamp9_larger(timestamp9, timestamp9) RETURNS timestamp9 AS 181 | '$libdir/timestamp9' 182 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 183 | 184 | CREATE AGGREGATE max(timestamp9) ( 185 | SFUNC = timestamp9_larger, 186 | STYPE = timestamp9, 187 | SORTOP = > 188 | ); 189 | 190 | CREATE FUNCTION timestamp9_smaller(timestamp9, timestamp9) RETURNS timestamp9 AS 191 | '$libdir/timestamp9' 192 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 193 | CREATE AGGREGATE min(timestamp9) ( 194 | SFUNC = timestamp9_smaller, 195 | STYPE = timestamp9, 196 | SORTOP = < 197 | ); 198 | 199 | CREATE FUNCTION timestamp9_interval_pl(timestamp9, interval) RETURNS timestamp9 AS 200 | '$libdir/timestamp9' 201 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 202 | CREATE OPERATOR + ( 203 | LEFTARG = timestamp9, 204 | RIGHTARG = interval, 205 | PROCEDURE = timestamp9_interval_pl, 206 | COMMUTATOR = + 207 | ); 208 | 209 | CREATE FUNCTION timestamp9_interval_mi(timestamp9, interval) RETURNS timestamp9 AS 210 | '$libdir/timestamp9' 211 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 212 | CREATE OPERATOR - ( 213 | LEFTARG = timestamp9, 214 | RIGHTARG = interval, 215 | PROCEDURE = timestamp9_interval_mi 216 | ); 217 | 218 | CREATE FUNCTION interval_timestamp9_pl(interval, timestamp9) RETURNS timestamp9 AS 219 | '$libdir/timestamp9' 220 | LANGUAGE c IMMUTABLE STRICT PARALLEL SAFE LEAKPROOF; 221 | CREATE OPERATOR + ( 222 | LEFTARG = interval, 223 | RIGHTARG = timestamp9, 224 | PROCEDURE = interval_timestamp9_pl, 225 | COMMUTATOR = + 226 | ); 227 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(PRIMARY_TEST_DIR ${CMAKE_CURRENT_LIST_DIR}) 2 | set(PRIMARY_TEST_DIR ${CMAKE_CURRENT_LIST_DIR} PARENT_SCOPE) 3 | set(_local_install_checks) 4 | set(_install_checks) 5 | 6 | # Testing support 7 | find_program(PG_REGRESS pg_regress 8 | HINTS 9 | "${PG_BINDIR}" 10 | "${PG_PKGLIBDIR}/pgxs/src/test/regress/") 11 | 12 | find_program(PG_ISOLATION_REGRESS 13 | NAMES pg_isolation_regress 14 | HINTS 15 | ${PG_BINDIR} 16 | ${PG_PKGLIBDIR}/pgxs/src/test/isolation 17 | ${PG_SOURCE_DIR}/src/test/isolation 18 | ${BINDIR}) 19 | 20 | include(test-defs.cmake) 21 | 22 | if(PG_REGRESS) 23 | message(STATUS "Using pg_regress ${PG_REGRESS}") 24 | 25 | add_custom_target(regresscheck 26 | COMMAND ${CMAKE_COMMAND} -E env 27 | ${PG_REGRESS_ENV} 28 | ${CMAKE_CURRENT_SOURCE_DIR}/pg_regress.sh 29 | ${PG_REGRESS_OPTS_BASE} 30 | ${PG_REGRESS_OPTS_EXTRA} 31 | ${PG_REGRESS_OPTS_INOUT} 32 | ${PG_REGRESS_OPTS_TEMP_INSTANCE} 33 | USES_TERMINAL) 34 | 35 | add_custom_target(regresschecklocal 36 | COMMAND ${CMAKE_COMMAND} -E env 37 | ${PG_REGRESS_ENV} 38 | ${CMAKE_CURRENT_SOURCE_DIR}/pg_regress.sh 39 | ${PG_REGRESS_OPTS_BASE} 40 | ${PG_REGRESS_OPTS_EXTRA} 41 | ${PG_REGRESS_OPTS_INOUT} 42 | ${PG_REGRESS_OPTS_LOCAL_INSTANCE} 43 | USES_TERMINAL) 44 | 45 | list(APPEND _local_install_checks regresschecklocal) 46 | list(APPEND _install_checks regresscheck) 47 | elseif(REGRESS_CHECKS) 48 | message(FATAL_ERROR 49 | "Program 'pg_regress' not found, but regressions checks were required.\n" 50 | "Skip regression checks using -DREGRESS_CHECKS=OFF") 51 | else() 52 | message(STATUS "Skipping regress checks since program 'pg_regress' was not found") 53 | endif() 54 | 55 | add_subdirectory(sql) 56 | add_subdirectory(expected) 57 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | In order to run the tests, build everything and make sure PG_REGRESS is found. Then run: 2 | 3 | ``` 4 | make regresscheck 5 | ``` 6 | 7 | To add new tests, either add them to an existing `.sql` file, or add a new `.sql` file. 8 | Make sure to update the `.out` file in the `expected` directory with the expected output. 9 | 10 | -------------------------------------------------------------------------------- /tests/expected/CMakeLists.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optiver/timestamp9/2d689f894fae0dfee2bae0e6b1b5b78f67e7d67c/tests/expected/CMakeLists.txt -------------------------------------------------------------------------------- /tests/expected/basics.out: -------------------------------------------------------------------------------- 1 | -- Set timezone to UTC-2 so that timestamp9 can have consistent timezone configuration 2 | -- on machines in different timezones. 3 | set timezone to 'UTC-2'; 4 | -- Test that we are able to convert nanoseconds to timestamp9. 5 | select 0::bigint::timestamp9; 6 | timestamp9 7 | ------------------------------------- 8 | 1970-01-01 02:00:00.000000000 +0200 9 | (1 row) 10 | 11 | select 9223372036854775807::timestamp9; 12 | timestamp9 13 | ------------------------------------- 14 | 2262-04-12 01:47:16.854775807 +0200 15 | (1 row) 16 | 17 | -- Test that we are able to convert various formats of timestamps to timestamp9 type. 18 | select '2019-09-19 08:30:05.123456789 +0200'::timestamp9; 19 | timestamp9 20 | ------------------------------------- 21 | 2019-09-19 08:30:05.123456789 +0200 22 | (1 row) 23 | 24 | select '2019-09-19 08:30:05.123456789-0200'::timestamp9; 25 | timestamp9 26 | ------------------------------------- 27 | 2019-09-19 12:30:05.123456789 +0200 28 | (1 row) 29 | 30 | select '2019-09-19 08:30:05.123456789+02:00'::timestamp9; 31 | timestamp9 32 | ------------------------------------- 33 | 2019-09-19 08:30:05.123456789 +0200 34 | (1 row) 35 | 36 | select '2019-09-19 08:30:05.123456789 -02:00'::timestamp9; 37 | timestamp9 38 | ------------------------------------- 39 | 2019-09-19 12:30:05.123456789 +0200 40 | (1 row) 41 | 42 | select '2019-09-19 08:30:05'::timestamp9; 43 | timestamp9 44 | ------------------------------------- 45 | 2019-09-19 08:30:05.000000000 +0200 46 | (1 row) 47 | 48 | -- Test that we are able to control the timezone of timestamp9 via the 'timezone' GUC. 49 | set timezone to 'UTC-8'; 50 | select '2019-09-19 08:30:05 +0800'::timestamp9; 51 | timestamp9 52 | ------------------------------------- 53 | 2019-09-19 08:30:05.000000000 +0800 54 | (1 row) 55 | 56 | set timezone to 'Europe/London'; 57 | select '2019-09-19 08:30:05 +0100'::timestamp9; 58 | timestamp9 59 | ------------------------------------- 60 | 2019-09-19 08:30:05.000000000 +0100 61 | (1 row) 62 | 63 | -- NOTE: If we don't specify the timezone when parsing the time, it follows the timezone 64 | -- of the current session by default. 65 | select '2019-09-19 08:30:05'::timestamp9; 66 | timestamp9 67 | ------------------------------------- 68 | 2019-09-19 08:30:05.000000000 +0100 69 | (1 row) 70 | 71 | select '2019-09-19 08:30:05.123456789'::timestamp9; 72 | timestamp9 73 | ------------------------------------- 74 | 2019-09-19 08:30:05.123456789 +0100 75 | (1 row) 76 | 77 | -- Test that we can use various timezones. 78 | select '2019-09-19 08:30:05.123456789 Europe/London'::timestamp9; 79 | timestamp9 80 | ------------------------------------- 81 | 2019-09-19 08:30:05.123456789 +0100 82 | (1 row) 83 | 84 | select '2019-09-19 08:30:05.123456789 utc-2'::timestamp9; 85 | timestamp9 86 | ------------------------------------- 87 | 2019-09-19 07:30:05.123456789 +0100 88 | (1 row) 89 | 90 | set timezone to 'UTC-2'; 91 | -- Test that we are able to reject bad inputs. 92 | select '2022-01-25 00:00:00.123456789 +'::timestamp9; 93 | ERROR: invalid input syntax for type numeric timezone: "+" at character 8 94 | select '2022-01-25 00:00:00.123456789 abcd'::timestamp9; 95 | ERROR: time zone "abcd" not recognized at character 8 96 | -- Test that we are able to compare timestamp9 values. 97 | select '2019-09-19'::timestamp9 < '2019-09-20'::timestamp9, greatest('2020-06-06'::timestamp9, '2019-01-01'::timestamp9); 98 | ?column? | greatest 99 | ----------+------------------------------------- 100 | t | 2020-06-06 00:00:00.000000000 +0200 101 | (1 row) 102 | 103 | select '2019-09-19'::timestamp9 > '2019-09-20'::timestamp9, least('2020-06-06'::timestamp9, '2019-01-01'::timestamp9); 104 | ?column? | least 105 | ----------+------------------------------------- 106 | f | 2019-01-01 00:00:00.000000000 +0200 107 | (1 row) 108 | 109 | select '2019-09-19'::timestamp9 >= '2019-09-20'::timestamp9; 110 | ?column? 111 | ---------- 112 | f 113 | (1 row) 114 | 115 | select '2019-09-20'::timestamp9 >= '2019-09-20'::timestamp9; 116 | ?column? 117 | ---------- 118 | t 119 | (1 row) 120 | 121 | select '2019-09-19'::timestamp9 <= '2019-09-20'::timestamp9; 122 | ?column? 123 | ---------- 124 | t 125 | (1 row) 126 | 127 | select '2019-09-20'::timestamp9 <= '2019-09-20'::timestamp9; 128 | ?column? 129 | ---------- 130 | t 131 | (1 row) 132 | 133 | select '2019-09-19'::timestamp9 = '2019-09-20'::timestamp9; 134 | ?column? 135 | ---------- 136 | f 137 | (1 row) 138 | 139 | select '2019-09-20'::timestamp9 = '2019-09-20'::timestamp9; 140 | ?column? 141 | ---------- 142 | t 143 | (1 row) 144 | 145 | -- Test that we can do arith calculations with timestamp9. 146 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 + interval '1 year'; 147 | ?column? 148 | ------------------------------------- 149 | 2020-09-19 23:00:00.123456789 +0200 150 | (1 row) 151 | 152 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 - interval '2 years'; 153 | ?column? 154 | ------------------------------------- 155 | 2017-09-19 23:00:00.123456789 +0200 156 | (1 row) 157 | 158 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 + interval '1 mon'; 159 | ?column? 160 | ------------------------------------- 161 | 2019-10-19 23:00:00.123456789 +0200 162 | (1 row) 163 | 164 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 - interval '2 mons'; 165 | ?column? 166 | ------------------------------------- 167 | 2019-07-19 23:00:00.123456789 +0200 168 | (1 row) 169 | 170 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 + interval '1 day'; 171 | ?column? 172 | ------------------------------------- 173 | 2019-09-20 23:00:00.123456789 +0200 174 | (1 row) 175 | 176 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 - interval '2 days'; 177 | ?column? 178 | ------------------------------------- 179 | 2019-09-17 23:00:00.123456789 +0200 180 | (1 row) 181 | 182 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 + interval '1 hour'; 183 | ?column? 184 | ------------------------------------- 185 | 2019-09-20 00:00:00.123456789 +0200 186 | (1 row) 187 | 188 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 - interval '2 hours'; 189 | ?column? 190 | ------------------------------------- 191 | 2019-09-19 21:00:00.123456789 +0200 192 | (1 row) 193 | 194 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 + interval '1 min'; 195 | ?column? 196 | ------------------------------------- 197 | 2019-09-19 23:01:00.123456789 +0200 198 | (1 row) 199 | 200 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 - interval '2 mins'; 201 | ?column? 202 | ------------------------------------- 203 | 2019-09-19 22:58:00.123456789 +0200 204 | (1 row) 205 | 206 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 + interval '1 sec'; 207 | ?column? 208 | ------------------------------------- 209 | 2019-09-19 23:00:01.123456789 +0200 210 | (1 row) 211 | 212 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 - interval '2 secs'; 213 | ?column? 214 | ------------------------------------- 215 | 2019-09-19 22:59:58.123456789 +0200 216 | (1 row) 217 | 218 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 + interval '1 millisecond'; 219 | ?column? 220 | ------------------------------------- 221 | 2019-09-19 23:00:00.124456789 +0200 222 | (1 row) 223 | 224 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 - interval '2 milliseconds'; 225 | ?column? 226 | ------------------------------------- 227 | 2019-09-19 23:00:00.121456789 +0200 228 | (1 row) 229 | 230 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 + interval '1 microsecond'; 231 | ?column? 232 | ------------------------------------- 233 | 2019-09-19 23:00:00.123457789 +0200 234 | (1 row) 235 | 236 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 - interval '2 microseconds'; 237 | ?column? 238 | ------------------------------------- 239 | 2019-09-19 23:00:00.123454789 +0200 240 | (1 row) 241 | 242 | CREATE TABLE tbl(ts timestamp9); 243 | CREATE INDEX ON tbl USING hash (ts); 244 | CREATE TABLE tbl1(ts timestamp9) PARTITION BY HASH (ts); 245 | -------------------------------------------------------------------------------- /tests/pg_regress.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Wrapper around pg_regress to be able to override the tests to run via the 4 | # TESTS environment variable 5 | 6 | # NB this script mirrors the adjacent pg_isolation_regress.sh, and they should 7 | # be kept in sync 8 | 9 | CURRENT_DIR=$(dirname $0) 10 | EXE_DIR=${EXE_DIR:-${CURRENT_DIR}} 11 | PG_REGRESS=${PG_REGRESS:-pg_regress} 12 | PG_REGRESS_DIFF_OPTS=-u 13 | TEST_SCHEDULE=${TEST_SCHEDULE:-} 14 | TEMP_SCHEDULE=${CURRENT_DIR}/temp_schedule 15 | TESTS=${TESTS:-} 16 | IGNORES=${IGNORES:-} 17 | SKIPS=${SKIPS:-} 18 | 19 | contains() { 20 | # a list contains a value foo if the regex ".* foo .*" holds true 21 | [[ $1 =~ (.*[[:space:]]|^)$2([[:space:]].*|$) ]]; 22 | return $? 23 | } 24 | 25 | echo "TESTS ${TESTS}" 26 | echo "IGNORES ${IGNORES}" 27 | echo "SKIPS ${SKIPS}" 28 | 29 | if [[ -z ${TESTS} ]]; then 30 | if [[ -z ${TEST_SCHEDULE} ]]; then 31 | for t in ${EXE_DIR}/sql/*.sql; do 32 | t=${t##${EXE_DIR}/sql/} 33 | t=${t%.sql} 34 | 35 | if ! contains "${SKIPS}" "${t}"; then 36 | TESTS="${TESTS} ${t}" 37 | fi 38 | done 39 | elif [[ -n ${IGNORES} ]] || [[ -n ${SKIPS} ]]; then 40 | # get the tests from the test schedule, but ignore our IGNORES 41 | while read t; do 42 | if [[ t =~ ignore:* ]]; then 43 | t=${t##ignore:* } 44 | IGNORES="${t} ${IGNORES}" 45 | continue 46 | fi 47 | t=${t##test: } 48 | ## check each individual test in test group to see if it should be ignored 49 | for el in ${t[@]}; do 50 | if ! contains "${SKIPS}" "${el}"; then 51 | TESTS="${TESTS} ${el}" 52 | fi 53 | done 54 | done < ${TEST_SCHEDULE} 55 | # no longer needed as the contents has been parsed above 56 | # and unsetting it helps take a shortcut later 57 | TEST_SCHEDULE= 58 | else 59 | PG_REGRESS_OPTS="${PG_REGRESS_OPTS} --schedule=${TEST_SCHEDULE}" 60 | fi 61 | else 62 | # Both this and pg_isolation_regress.sh use the same TESTS env var to decide which tests to run. 63 | # Since we only want to pass the test runner the kind of tests it can understand, 64 | # and only those which actually exist, we use TESTS as a filter for the test folder, 65 | # passing in only those tests from the directory which are found in TESTS 66 | FILTER=${TESTS} 67 | TESTS= 68 | for t in ${EXE_DIR}/sql/*.sql; do 69 | t=${t##${EXE_DIR}/sql/} 70 | t=${t%.sql} 71 | 72 | if contains "${FILTER}" "${t}" && ! contains "${SKIPS}" "${t}"; then 73 | TESTS="${TESTS} $t" 74 | fi 75 | done 76 | # When TESTS is specified the passed tests scheduler is not used and emptying it helps take a shortcut later 77 | TEST_SCHEDULE= 78 | fi 79 | 80 | if [[ -z ${TESTS} ]] && [[ -z ${TEST_SCHEDULE} ]]; then 81 | exit 0; 82 | fi 83 | 84 | function cleanup() { 85 | rm -rf ${EXE_DIR}/sql/dump 86 | rm -rf ${TEST_TABLESPACE1_PATH} 87 | rm -rf ${TEST_TABLESPACE2_PATH} 88 | rm -f ${CURRENT_DIR}/.pg_init 89 | rm -f ${TEMP_SCHEDULE} 90 | } 91 | 92 | trap cleanup EXIT 93 | 94 | # This mktemp line will work on both OSX and GNU systems 95 | TEST_TABLESPACE1_PATH=${TEST_TABLESPACE1_PATH:-$(mktemp -d 2>/dev/null || mktemp -d -t 'timescaledb_regress')} 96 | TEST_TABLESPACE2_PATH=${TEST_TABLESPACE2_PATH:-$(mktemp -d 2>/dev/null || mktemp -d -t 'timescaledb_regress')} 97 | export TEST_TABLESPACE1_PATH TEST_TABLESPACE2_PATH 98 | 99 | rm -f ${TEST_OUTPUT_DIR}/.pg_init 100 | mkdir -p ${EXE_DIR}/sql/dump 101 | 102 | export PG_REGRESS_DIFF_OPTS 103 | 104 | touch ${TEMP_SCHEDULE} 105 | rm ${TEMP_SCHEDULE} 106 | touch ${TEMP_SCHEDULE} 107 | 108 | for t in ${IGNORES}; do 109 | echo "ignore: ${t}" >> ${TEMP_SCHEDULE} 110 | done 111 | 112 | for t in ${TESTS}; do 113 | echo "test: ${t}" >> ${TEMP_SCHEDULE} 114 | done 115 | 116 | PG_REGRESS_OPTS="${PG_REGRESS_OPTS} --schedule=${TEMP_SCHEDULE}" 117 | ${PG_REGRESS} $@ ${PG_REGRESS_OPTS} 118 | -------------------------------------------------------------------------------- /tests/postgresql.conf: -------------------------------------------------------------------------------- 1 | autovacuum=false 2 | random_page_cost=1.0 3 | log_line_prefix='%u [%p] %d ' 4 | -------------------------------------------------------------------------------- /tests/runner.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -u 4 | set -e 5 | CURRENT_DIR=$(dirname $0) 6 | EXE_DIR=${EXE_DIR:-${CURRENT_DIR}} 7 | PG_REGRESS_PSQL=$1 8 | PSQL=${PSQL:-$PG_REGRESS_PSQL} 9 | PSQL="${PSQL} -X" # Prevent any .psqlrc files from being executed during the tests 10 | TEST_PGUSER=${TEST_PGUSER:-postgres} 11 | TEST_INPUT_DIR=${TEST_INPUT_DIR:-${EXE_DIR}} 12 | TEST_OUTPUT_DIR=${TEST_OUTPUT_DIR:-${EXE_DIR}} 13 | 14 | # PGAPPNAME will be 'pg_regress/test' so we cut off the prefix 15 | # to get the name of the test (PG 10 and 11 only) 16 | if [[ ${PGAPPNAME} = pg_regress/* ]]; then 17 | CURRENT_TEST=${PGAPPNAME##pg_regress/} 18 | else 19 | # PG 9.6 pg_regress does not pass in testname 20 | # so we generate unique name from pid 21 | CURRENT_TEST="test_$$" 22 | fi 23 | TEST_DBNAME="db_${CURRENT_TEST}" 24 | 25 | # Read the extension version from version.config 26 | read -r VERSION < ${CURRENT_DIR}/../version.config 27 | EXT_VERSION=${VERSION##version = } 28 | 29 | #docker doesn't set user 30 | USER=${USER:-`whoami`} 31 | 32 | TEST_SPINWAIT_ITERS=${TEST_SPINWAIT_ITERS:-100} 33 | 34 | TEST_ROLE_SUPERUSER=${TEST_ROLE_SUPERUSER:-super_user} 35 | TEST_ROLE_DEFAULT_PERM_USER=${TEST_ROLE_DEFAULT_PERM_USER:-default_perm_user} 36 | TEST_ROLE_DEFAULT_PERM_USER_2=${TEST_ROLE_DEFAULT_PERM_USER_2:-default_perm_user_2} 37 | 38 | shift 39 | 40 | function cleanup { 41 | ${PSQL} $@ -U $TEST_ROLE_SUPERUSER -d postgres -v ECHO=none -c "DROP DATABASE \"${TEST_DBNAME}\";" >/dev/null 42 | } 43 | 44 | trap cleanup EXIT 45 | 46 | # setup clusterwide settings on first run 47 | if [[ ! -f ${TEST_OUTPUT_DIR}/.pg_init ]]; then 48 | touch ${TEST_OUTPUT_DIR}/.pg_init 49 | ${PSQL} $@ -U ${USER} -d postgres -v ECHO=none -c "ALTER USER ${TEST_ROLE_SUPERUSER} WITH SUPERUSER;" >/dev/null 50 | fi 51 | 52 | cd ${EXE_DIR}/sql 53 | 54 | # create database and install timestamp9 55 | ${PSQL} $@ -U $TEST_ROLE_SUPERUSER -d postgres -v ECHO=none -c "CREATE DATABASE \"${TEST_DBNAME}\";" 56 | ${PSQL} $@ -U $TEST_ROLE_SUPERUSER -d ${TEST_DBNAME} -v ECHO=none -c "SET client_min_messages=error; CREATE EXTENSION timestamp9; \ 57 | GRANT CREATE ON SCHEMA public TO PUBLIC;" 58 | 59 | export TEST_DBNAME 60 | 61 | # we strip out any output between 62 | # and the part about memory usage in EXPLAIN ANALYZE output of Sort nodes 63 | ${PSQL} -U ${TEST_PGUSER} \ 64 | -v ON_ERROR_STOP=0 \ 65 | -v VERBOSITY=terse \ 66 | -v ECHO=all \ 67 | -v DISABLE_OPTIMIZATIONS=off \ 68 | -v TEST_DBNAME="${TEST_DBNAME}" \ 69 | -v TEST_TABLESPACE1_PATH=\'${TEST_TABLESPACE1_PATH}\' \ 70 | -v TEST_TABLESPACE2_PATH=\'${TEST_TABLESPACE2_PATH}\' \ 71 | -v TEST_INPUT_DIR=${TEST_INPUT_DIR} \ 72 | -v TEST_OUTPUT_DIR=${TEST_OUTPUT_DIR} \ 73 | -v TEST_SPINWAIT_ITERS=${TEST_SPINWAIT_ITERS} \ 74 | -v ROLE_SUPERUSER=${TEST_ROLE_SUPERUSER} \ 75 | -v ROLE_DEFAULT_PERM_USER=${TEST_ROLE_DEFAULT_PERM_USER} \ 76 | -v ROLE_DEFAULT_PERM_USER_2=${TEST_ROLE_DEFAULT_PERM_USER_2} \ 77 | $@ -d ${TEST_DBNAME} 2>&1 | sed -e '//,/<\/exclude_from_test>/d' -e 's! Memory: [0-9]\{1,\}kB!!' -e 's! Memory Usage: [0-9]\{1,\}kB!!' 78 | -------------------------------------------------------------------------------- /tests/runner_shared.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -u 4 | set -e 5 | CURRENT_DIR=$(dirname $0) 6 | EXE_DIR=${EXE_DIR:-${CURRENT_DIR}} 7 | PG_REGRESS_PSQL=$1 8 | PSQL=${PSQL:-$PG_REGRESS_PSQL} 9 | PSQL="${PSQL} -X" # Prevent any .psqlrc files from being executed during the tests 10 | TEST_PGUSER=${TEST_PGUSER:-postgres} 11 | TEST_INPUT_DIR=${TEST_INPUT_DIR:-${EXE_DIR}} 12 | TEST_OUTPUT_DIR=${TEST_OUTPUT_DIR:-${EXE_DIR}} 13 | 14 | # PGAPPNAME will be 'pg_regress/test' so we cut off the prefix 15 | # to get the name of the test (PG 10 and 11 only) 16 | TEST_BASE_NAME=${PGAPPNAME##pg_regress/} 17 | 18 | # if this is a versioned test our name will have version as suffix 19 | # so we cut off suffix to get base name 20 | if [[ ${TEST_BASE_NAME} == *-1[0-9] ]]; then 21 | TEST_BASE_NAME=${TEST_BASE_NAME%???} 22 | fi 23 | 24 | #docker doesn't set user 25 | USER=${USER:-`whoami`} 26 | 27 | TEST_ROLE_SUPERUSER=${TEST_ROLE_SUPERUSER:-super_user} 28 | TEST_ROLE_DEFAULT_PERM_USER=${TEST_ROLE_DEFAULT_PERM_USER:-default_perm_user} 29 | TEST_ROLE_DEFAULT_PERM_USER_2=${TEST_ROLE_DEFAULT_PERM_USER_2:-default_perm_user_2} 30 | 31 | shift 32 | 33 | # setup clusterwide settings on first run 34 | if [[ ! -f ${TEST_OUTPUT_DIR}/.pg_init ]]; then 35 | touch ${TEST_OUTPUT_DIR}/.pg_init 36 | ${PSQL} $@ -U ${USER} -d postgres -v ECHO=none -c "ALTER USER ${TEST_ROLE_SUPERUSER} WITH SUPERUSER;" >/dev/null 37 | ${PSQL} $@ -U $TEST_PGUSER -d ${TEST_DBNAME} -v ECHO=none < ${TEST_INPUT_DIR}/shared/sql/include/shared_setup.sql >/dev/null 38 | fi 39 | 40 | cd ${EXE_DIR}/sql 41 | 42 | # we strip out any output between 43 | # and the part about memory usage in EXPLAIN ANALYZE output of Sort nodes 44 | ${PSQL} -U ${TEST_PGUSER} \ 45 | -v ON_ERROR_STOP=0 \ 46 | -v VERBOSITY=terse \ 47 | -v ECHO=all \ 48 | -v TEST_BASE_NAME=${TEST_BASE_NAME} \ 49 | -v TEST_INPUT_DIR=${TEST_INPUT_DIR} \ 50 | -v TEST_OUTPUT_DIR=${TEST_OUTPUT_DIR} \ 51 | -v ROLE_SUPERUSER=${TEST_ROLE_SUPERUSER} \ 52 | -v ROLE_DEFAULT_PERM_USER=${TEST_ROLE_DEFAULT_PERM_USER} \ 53 | -v ROLE_DEFAULT_PERM_USER_2=${TEST_ROLE_DEFAULT_PERM_USER_2} \ 54 | $@ -d ${TEST_DBNAME} 2>&1 | sed -e '//,/<\/exclude_from_test>/d' -e 's! Memory: [0-9]\{1,\}kB!!' -e 's! Memory Usage: [0-9]\{1,\}kB!!' 55 | -------------------------------------------------------------------------------- /tests/sql/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(TEST_FILES 2 | basics.sql 3 | ) 4 | 5 | if(NOT TEST_GROUP_SIZE) 6 | set(PARALLEL_GROUP_SIZE 20) 7 | else() 8 | set(PARALLEL_GROUP_SIZE ${TEST_GROUP_SIZE}) 9 | endif() 10 | 11 | set(GROUP_MEMBERS 0) 12 | 13 | list(SORT TEST_FILES) 14 | file(REMOVE ${TEST_SCHEDULE}) 15 | 16 | foreach(TEST_FILE ${TEST_FILES}) 17 | string(REGEX REPLACE "(.+)\.sql" "\\1" TESTS_TO_RUN ${TEST_FILE}) 18 | if(GROUP_MEMBERS EQUAL 0) 19 | file(APPEND ${TEST_SCHEDULE} "\ntest: ") 20 | endif() 21 | file(APPEND ${TEST_SCHEDULE} "${TESTS_TO_RUN} ") 22 | MATH(EXPR GROUP_MEMBERS "(${GROUP_MEMBERS}+1)%${PARALLEL_GROUP_SIZE}") 23 | endforeach(TEST_FILE) 24 | 25 | file(APPEND ${TEST_SCHEDULE} "\n") 26 | -------------------------------------------------------------------------------- /tests/sql/basics.sql: -------------------------------------------------------------------------------- 1 | -- Set timezone to UTC-2 so that timestamp9 can have consistent timezone configuration 2 | -- on machines in different timezones. 3 | set timezone to 'UTC-2'; 4 | 5 | -- Test that we are able to convert nanoseconds to timestamp9. 6 | select 0::bigint::timestamp9; 7 | select 9223372036854775807::timestamp9; 8 | 9 | -- Test that we are able to convert various formats of timestamps to timestamp9 type. 10 | select '2019-09-19 08:30:05.123456789 +0200'::timestamp9; 11 | select '2019-09-19 08:30:05.123456789-0200'::timestamp9; 12 | select '2019-09-19 08:30:05.123456789+02:00'::timestamp9; 13 | select '2019-09-19 08:30:05.123456789 -02:00'::timestamp9; 14 | select '2019-09-19 08:30:05'::timestamp9; 15 | 16 | -- Test that we are able to control the timezone of timestamp9 via the 'timezone' GUC. 17 | set timezone to 'UTC-8'; 18 | select '2019-09-19 08:30:05 +0800'::timestamp9; 19 | set timezone to 'Europe/London'; 20 | select '2019-09-19 08:30:05 +0100'::timestamp9; 21 | -- NOTE: If we don't specify the timezone when parsing the time, it follows the timezone 22 | -- of the current session by default. 23 | select '2019-09-19 08:30:05'::timestamp9; 24 | select '2019-09-19 08:30:05.123456789'::timestamp9; 25 | -- Test that we can use various timezones. 26 | select '2019-09-19 08:30:05.123456789 Europe/London'::timestamp9; 27 | select '2019-09-19 08:30:05.123456789 utc-2'::timestamp9; 28 | set timezone to 'UTC-2'; 29 | 30 | -- Test that we are able to reject bad inputs. 31 | select '2022-01-25 00:00:00.123456789 +'::timestamp9; 32 | select '2022-01-25 00:00:00.123456789 abcd'::timestamp9; 33 | 34 | -- Test that we are able to compare timestamp9 values. 35 | select '2019-09-19'::timestamp9 < '2019-09-20'::timestamp9, greatest('2020-06-06'::timestamp9, '2019-01-01'::timestamp9); 36 | select '2019-09-19'::timestamp9 > '2019-09-20'::timestamp9, least('2020-06-06'::timestamp9, '2019-01-01'::timestamp9); 37 | select '2019-09-19'::timestamp9 >= '2019-09-20'::timestamp9; 38 | select '2019-09-20'::timestamp9 >= '2019-09-20'::timestamp9; 39 | select '2019-09-19'::timestamp9 <= '2019-09-20'::timestamp9; 40 | select '2019-09-20'::timestamp9 <= '2019-09-20'::timestamp9; 41 | select '2019-09-19'::timestamp9 = '2019-09-20'::timestamp9; 42 | select '2019-09-20'::timestamp9 = '2019-09-20'::timestamp9; 43 | 44 | -- Test that we can do arith calculations with timestamp9. 45 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 + interval '1 year'; 46 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 - interval '2 years'; 47 | 48 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 + interval '1 mon'; 49 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 - interval '2 mons'; 50 | 51 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 + interval '1 day'; 52 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 - interval '2 days'; 53 | 54 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 + interval '1 hour'; 55 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 - interval '2 hours'; 56 | 57 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 + interval '1 min'; 58 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 - interval '2 mins'; 59 | 60 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 + interval '1 sec'; 61 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 - interval '2 secs'; 62 | 63 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 + interval '1 millisecond'; 64 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 - interval '2 milliseconds'; 65 | 66 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 + interval '1 microsecond'; 67 | select '2019-09-19 23:00:00.123456789 +0200'::timestamp9 - interval '2 microseconds'; 68 | 69 | CREATE TABLE tbl(ts timestamp9); 70 | CREATE INDEX ON tbl USING hash (ts); 71 | 72 | CREATE TABLE tbl1(ts timestamp9) PARTITION BY HASH (ts); 73 | -------------------------------------------------------------------------------- /tests/test-defs.cmake: -------------------------------------------------------------------------------- 1 | set(TEST_ROLE_SUPERUSER super_user) 2 | set(TEST_ROLE_DEFAULT_PERM_USER default_perm_user) 3 | set(TEST_ROLE_DEFAULT_PERM_USER_2 default_perm_user_2) 4 | 5 | set(TEST_INPUT_DIR ${CMAKE_CURRENT_SOURCE_DIR}) 6 | set(TEST_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}) 7 | set(TEST_CLUSTER ${TEST_OUTPUT_DIR}/testcluster) 8 | 9 | # Basic connection info for test instance 10 | set(TEST_PGPORT_LOCAL 5432 CACHE STRING "The port of a running PostgreSQL instance") 11 | set(TEST_PGHOST localhost CACHE STRING "The hostname of a running PostgreSQL instance") 12 | set(TEST_PGUSER ${TEST_ROLE_DEFAULT_PERM_USER} CACHE STRING "The PostgreSQL test user") 13 | set(TEST_DBNAME single CACHE STRING "The database name to use for tests") 14 | set(TEST_PGPORT_TEMP_INSTANCE 15432 CACHE STRING "The port to run a temporary test PostgreSQL instance on") 15 | set(TEST_SCHEDULE ${CMAKE_CURRENT_BINARY_DIR}/test_schedule) 16 | set(TEST_SCHEDULE_SHARED ${CMAKE_CURRENT_BINARY_DIR}/shared/test_schedule_shared) 17 | set(ISOLATION_TEST_SCHEDULE ${CMAKE_CURRENT_BINARY_DIR}/isolation_test_schedule) 18 | 19 | set(PG_REGRESS_OPTS_BASE 20 | --host=${TEST_PGHOST} 21 | --load-extension=plpgsql 22 | --dlpath=${PROJECT_BINARY_DIR}/src) 23 | 24 | set(PG_REGRESS_OPTS_EXTRA 25 | --create-role=${TEST_ROLE_SUPERUSER},${TEST_ROLE_DEFAULT_PERM_USER},${TEST_ROLE_DEFAULT_PERM_USER_2} 26 | --dbname=${TEST_DBNAME} 27 | --launcher=${PRIMARY_TEST_DIR}/runner.sh) 28 | 29 | set(PG_REGRESS_SHARED_OPTS_EXTRA 30 | --create-role=${TEST_ROLE_SUPERUSER},${TEST_ROLE_DEFAULT_PERM_USER},${TEST_ROLE_DEFAULT_PERM_USER_2} 31 | --dbname=${TEST_DBNAME} 32 | --launcher=${PRIMARY_TEST_DIR}/runner_shared.sh) 33 | 34 | set(PG_ISOLATION_REGRESS_OPTS_EXTRA 35 | --create-role=${TEST_ROLE_SUPERUSER},${TEST_ROLE_DEFAULT_PERM_USER},${TEST_ROLE_DEFAULT_PERM_USER_2} 36 | --dbname=${TEST_DBNAME}) 37 | 38 | set(PG_REGRESS_OPTS_INOUT 39 | --inputdir=${TEST_INPUT_DIR} 40 | --outputdir=${TEST_OUTPUT_DIR}) 41 | 42 | set(PG_REGRESS_SHARED_OPTS_INOUT 43 | --inputdir=${TEST_INPUT_DIR}/shared 44 | --outputdir=${TEST_OUTPUT_DIR}/shared 45 | --load-extension=timescaledb) 46 | 47 | set(PG_ISOLATION_REGRESS_OPTS_INOUT 48 | --inputdir=${TEST_INPUT_DIR}/isolation 49 | --outputdir=${TEST_OUTPUT_DIR}/isolation 50 | --load-extension=timescaledb) 51 | 52 | set(PG_REGRESS_OPTS_TEMP_INSTANCE 53 | --port=${TEST_PGPORT_TEMP_INSTANCE} 54 | --temp-instance=${TEST_CLUSTER} 55 | --temp-config=${TEST_INPUT_DIR}/postgresql.conf 56 | ) 57 | 58 | set(PG_REGRESS_OPTS_LOCAL_INSTANCE 59 | --port=${TEST_PGPORT_LOCAL}) 60 | 61 | if(PG_REGRESS) 62 | set(PG_REGRESS_ENV 63 | TEST_PGUSER=${TEST_PGUSER} 64 | TEST_PGHOST=${TEST_PGHOST} 65 | TEST_ROLE_SUPERUSER=${TEST_ROLE_SUPERUSER} 66 | TEST_ROLE_DEFAULT_PERM_USER=${TEST_ROLE_DEFAULT_PERM_USER} 67 | TEST_ROLE_DEFAULT_PERM_USER_2=${TEST_ROLE_DEFAULT_PERM_USER_2} 68 | TEST_DBNAME=${TEST_DBNAME} 69 | TEST_INPUT_DIR=${TEST_INPUT_DIR} 70 | TEST_OUTPUT_DIR=${TEST_OUTPUT_DIR} 71 | TEST_SCHEDULE=${TEST_SCHEDULE} 72 | PG_BINDIR=${PG_BINDIR} 73 | PG_REGRESS=${PG_REGRESS}) 74 | endif() 75 | 76 | if(PG_ISOLATION_REGRESS) 77 | set(PG_ISOLATION_REGRESS_ENV 78 | TEST_PGUSER=${TEST_PGUSER} 79 | TEST_ROLE_SUPERUSER=${TEST_ROLE_SUPERUSER} 80 | TEST_ROLE_DEFAULT_PERM_USER=${TEST_ROLE_DEFAULT_PERM_USER} 81 | TEST_ROLE_DEFAULT_PERM_USER_2=${TEST_ROLE_DEFAULT_PERM_USER_2} 82 | TEST_DBNAME=${TEST_DBNAME} 83 | TEST_INPUT_DIR=${TEST_INPUT_DIR} 84 | TEST_OUTPUT_DIR=${TEST_OUTPUT_DIR} 85 | ISOLATION_TEST_SCHEDULE=${ISOLATION_TEST_SCHEDULE} 86 | PG_ISOLATION_REGRESS=${PG_ISOLATION_REGRESS}) 87 | endif() 88 | 89 | if (${PG_VERSION_MAJOR} GREATER "9") 90 | set(TEST_VERSION_SUFFIX ${PG_VERSION_MAJOR}) 91 | else () 92 | set(TEST_VERSION_SUFFIX ${PG_VERSION_MAJOR}.${PG_VERSION_MINOR}) 93 | endif () 94 | 95 | -------------------------------------------------------------------------------- /timestamp9.control: -------------------------------------------------------------------------------- 1 | # timestamp9 extension 2 | comment = 'timestamp nanosecond resolution' 3 | default_version = '1.4.0' 4 | module_pathname = '$libdir/timestamp9' 5 | relocatable = true 6 | -------------------------------------------------------------------------------- /version.config: -------------------------------------------------------------------------------- 1 | version = 1.4.0 2 | update_from_version = 0.1.0 3 | --------------------------------------------------------------------------------