Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b328086
Make age extension usable from shared_preload_libraries (#2438)
serdarmumcu Jun 20, 2026
19a3b63
feature: add create_subgraph() (#2441)
jrgemignani Jun 20, 2026
1d55993
cypher_vle: add ORDER BY to non-deterministic RETURN queries (#2434)
jrgemignani Jun 20, 2026
7f45c7d
age_global_graph: stabilize regression tests (#2431)
jrgemignani Jun 20, 2026
22c6567
Makefile: add installcheck-existing target and improve readability (#…
jrgemignani Jun 20, 2026
a6b5471
Make ag_catalog ownership and built-in resolution explicit (#2440)
jrgemignani Jun 21, 2026
b53c32e
cypher_with: add ORDER BY to non-deterministic RETURN queries (#2436)
jrgemignani Jun 21, 2026
8690da2
Add shortest_path / all_shortest_paths SRFs (#2430)
jrgemignani Jun 21, 2026
88c1c33
Support pattern expressions as boolean expressions (#2360)
gregfelice Jun 22, 2026
2773080
Add reduce() list folding function (#2435)
jrgemignani Jun 26, 2026
bdbb717
resolve subgraph staging sequences via regclass (#2446)
jrgemignani Jun 29, 2026
7c7f3ae
Support relationship-type filters and a minimum hop count (#2442)
jrgemignani Jun 29, 2026
10e0f74
Fix single-node labeled pattern expressions not filtering by label (#…
gregfelice Jun 29, 2026
a7e10f4
Preserve null-valued keys in map literals (#2391) (#2412)
crprashant Jun 29, 2026
934ad25
Fix Node.js driver CI build broken by @types/node drift (#2452)
jrgemignani Jul 2, 2026
2e6db91
Fix stack overflow and precision loss in toFloatList() conversion (#2…
gregfelice Jul 2, 2026
d53c1b9
Support outer references in reduce() fold bodies (#2448)
jrgemignani Jul 2, 2026
373da2d
Fix segfault and out-of-bounds reads in file loaders on malformed row…
gregfelice Jul 2, 2026
8373dc8
ci: pin Build/Regression runner to ubuntu-24.04 and guard Bison versi…
gregfelice Jul 2, 2026
772c7eb
Add automatic header-dependency tracking to the Makefile (#2454)
jrgemignani Jul 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion .github/workflows/installcheck.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ on:

jobs:
build:
runs-on: ubuntu-latest
# Pinned (not ubuntu-latest) so the Bison version stays fixed at 3.8.x.
# The Cypher GLR grammar pins exact conflict counts via %expect / %expect-rr
# in src/backend/parser/cypher_gram.y, and Bison treats %expect as exact-match:
# a different Bison version can report different counts and break the build.
# Freezing the runner image freezes Bison; bump both together, intentionally.
runs-on: ubuntu-24.04

steps:
- name: Get latest commit id of PostgreSQL 19
Expand All @@ -28,6 +33,27 @@ jobs:
sudo apt-get update
sudo apt-get install -y build-essential libreadline-dev zlib1g-dev flex bison

- name: Verify Bison version (grammar conflict counts are pinned)
run: |
ver=$(bison --version | awk 'NR==1 {print $NF}')
if [ -z "$ver" ]; then
echo "::error::Could not determine Bison version from 'bison --version'."
echo "::error::Expected the first line to end with a version (e.g. '... 3.8.2')."
exit 1
fi
echo "bison $ver"
case "$ver" in
3.8.*) ;;
*)
echo "::error::Bison $ver != 3.8.x. The Cypher GLR grammar pins exact"
echo "::error::%expect / %expect-rr conflict counts in src/backend/parser/cypher_gram.y."
echo "::error::A new Bison version may report different counts. Re-run bison locally,"
echo "::error::update the %expect/%expect-rr numbers (and the comment block), then bump"
echo "::error::the pinned runner image and this guard together."
exit 1
;;
esac

- name: Install PostgreSQL 19 and some extensions
if: steps.pg19cache.outputs.cache-hit != 'true'
run: |
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/nodejs-driver.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ jobs:
run: docker compose up -d

- name: Set up Node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: latest
node-version: 20

- name: Install dependencies
run: npm install
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
*.o
*.d
*.so
build.sh
.idea
Expand Down
182 changes: 168 additions & 14 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,38 @@
# specific language governing permissions and limitations
# under the License.

# ===========================================================================
# Apache AGE extension build
#
# File layout (top to bottom):
# * Module
# * Upgrade regression-test support (1/2: variables)
# * Extension SQL & data files
# * Regression test suite (REGRESS / REGRESS_OPTS)
# * PGXS include
# * Build rules
# * Upgrade regression-test support (2/2: rules + installcheck lifecycle)
# * installcheck-existing (run against a running server)
# * help
#
# Common targets:
# all Build the extension (default)
# install Install into the PostgreSQL tree
# installcheck Run regression tests in a private temp instance
# installcheck-existing Run regression tests against a running server
# clean Remove build artifacts
# help Show the target list
# ===========================================================================

# ===== Module =====
MODULE_big = age

age_sql = age--1.7.0.sql

# --- Extension upgrade regression test support ---
# ===== Upgrade regression-test support (1/2: variables) =====
#
# This feature spans two sections (the PGXS include forces the split):
# * 1/2 (here, pre-include): variables -- must be defined before DATA,
# REGRESS, and EXTRA_CLEAN reference them.
# * 2/2 (below the PGXS include): build rules + installcheck lifecycle.
#
# Validates the upgrade template (age--<VER>--y.y.y.sql) by simulating an
# extension version upgrade entirely within "make installcheck". The test:
Expand Down Expand Up @@ -53,7 +80,7 @@ age_sql = age--1.7.0.sql
# (e.g., age--1.7.0--1.8.0.sql is committed): the synthetic test is
# redundant because the real script ships with the extension.
# Current version from age.control (e.g., "1.7.0")
AGE_CURR_VER := $(shell awk -F"'" '/default_version/ {print $$2}' age.control 2>/dev/null)
AGE_CURR_VER := $(shell awk -F"'" '/^default_version/ {print $$2}' age.control 2>/dev/null)
# Git commit that last changed age.control — the "initial release" commit
AGE_VER_COMMIT := $(shell git log -1 --format=%H -- age.control 2>/dev/null)
# Synthetic initial version: current version with _initial suffix
Expand All @@ -80,6 +107,7 @@ AGE_REAL_UPGRADE := $(shell git ls-files 'age--$(AGE_CURR_VER)--*.sql' 2>/dev/nu
# supersedes the synthetic one and has its own validation path.
AGE_HAS_UPGRADE_TEST = $(and $(AGE_VER_COMMIT),$(AGE_UPGRADE_TEMPLATE),$(if $(AGE_REAL_UPGRADE),,yes))

# ===== Object files =====
OBJS = src/backend/age.o \
src/backend/catalog/ag_catalog.o \
src/backend/catalog/ag_graph.o \
Expand Down Expand Up @@ -134,6 +162,11 @@ OBJS = src/backend/age.o \
src/backend/utils/name_validation.o \
src/backend/utils/ag_guc.o

# Per-object header-dependency files (see "Automatic header-dependency
# tracking" below the PGXS include). One .d is generated beside each .o.
DEPFILES = $(OBJS:.o=.d)

# ===== Extension SQL & data files =====
EXTENSION = age

# to allow cleaning of previous (old) age--.sql files
Expand All @@ -143,12 +176,18 @@ SQLS := $(shell cat sql/sql_files)
SQLS := $(addprefix sql/,$(SQLS))
SQLS := $(addsuffix .sql,$(SQLS))

# Name of the generated install SQL (age--<version>.sql).
# Derived from AGE_CURR_VER (read from age.control above) so the version
# number lives in exactly one place.
age_sql = age--$(AGE_CURR_VER).sql

DATA_built = $(age_sql)

# Git-tracked upgrade scripts shipped with the extension (e.g., age--1.6.0--1.7.0.sql).
# Excludes the upgrade template (y.y.y) and the synthetic stamped test file.
DATA = $(filter-out age--%-y.y.y.sql $(age_upgrade_test_sql),$(wildcard age--*--*.sql))
DATA = $(filter-out age--%--y.y.y.sql $(age_upgrade_test_sql),$(wildcard age--*--*.sql))

# ===== Regression test suite =====
# sorted in dependency order
REGRESS = scan \
graphid \
Expand All @@ -166,6 +205,7 @@ REGRESS = scan \
cypher_delete \
cypher_with \
cypher_vle \
age_shortest_path \
cypher_union \
cypher_call \
cypher_merge \
Expand All @@ -179,12 +219,16 @@ REGRESS = scan \
jsonb_operators \
list_comprehension \
predicate_functions \
pattern_expression \
age_reduce \
map_projection \
direct_field_access \
security \
reserved_keyword_alias \
agtype_jsonb_cast \
containment_selectivity
containment_selectivity \
subgraph \
extension_security

ifneq ($(EXTRA_TESTS),)
REGRESS += $(EXTRA_TESTS)
Expand All @@ -202,21 +246,55 @@ REGRESS += drop
srcdir=`pwd`

ag_regress_dir = $(srcdir)/regress
REGRESS_OPTS = --load-extension=age --inputdir=$(ag_regress_dir) --outputdir=$(ag_regress_dir) --temp-instance=$(ag_regress_dir)/instance --port=61958 --encoding=UTF-8 --temp-config $(ag_regress_dir)/age_regression.conf
REGRESS_OPTS = --load-extension=age \
--inputdir=$(ag_regress_dir) \
--outputdir=$(ag_regress_dir) \
--temp-instance=$(ag_regress_dir)/instance \
--port=61958 \
--encoding=UTF-8 \
--temp-config $(ag_regress_dir)/age_regression.conf

ag_regress_out = instance/ log/ results/ regression.*
EXTRA_CLEAN = $(addprefix $(ag_regress_dir)/, $(ag_regress_out)) src/backend/parser/cypher_gram.c src/include/parser/cypher_gram_def.h src/include/parser/cypher_kwlist_d.h $(all_age_sql) $(age_init_sql) $(age_upgrade_test_sql) $(ag_regress_dir)/age_upgrade_cleanup.sh
EXTRA_CLEAN = $(addprefix $(ag_regress_dir)/, $(ag_regress_out)) \
src/backend/parser/cypher_gram.c \
src/include/parser/cypher_gram_def.h \
src/include/parser/cypher_kwlist_d.h \
$(all_age_sql) \
$(age_init_sql) \
$(age_upgrade_test_sql) \
$(ag_regress_dir)/age_upgrade_cleanup.sh \
$(DEPFILES)

GEN_KEYWORDLIST = $(PERL) -I ./tools/ ./tools/gen_keywordlist.pl
GEN_KEYWORDLIST_DEPS = ./tools/gen_keywordlist.pl tools/PerfectHash.pm

ag_include_dir = $(srcdir)/src/include
PG_CPPFLAGS = -I$(ag_include_dir) -I$(ag_include_dir)/parser

# ===== PGXS =====
PG_CONFIG ?= pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)

# ===== Automatic header-dependency tracking =====
#
# AGE lists OBJS explicitly, and PGXS's built-in .deps tracking only runs when
# the *server* was built with --enable-depend (often off). Without the lines
# below, editing a header does NOT rebuild the .c files that include it, leaving
# STALE .o files. This is especially dangerous for node/struct headers: a stale
# ag_nodes.o keeps an old node_size, so _readExtensibleNode under-allocates and
# readNode corrupts the heap ("unrecognized node type: <garbage>").
#
# The compiler emits a .d file next to each object (-MMD = user headers only;
# -MP adds phony targets so deleting a header does not break the build). With
# "-o foo.o", -MMD writes "foo.d" automatically (no -MF, no basename clashes).
# On servers that DO set --enable-depend, PGXS appends its own "-MF .deps/*.Po"
# after $(CFLAGS) (last -MF wins), so this degrades cleanly to that mechanism.
override CFLAGS += -MMD -MP
-include $(DEPFILES)

# ===== Build rules =====

# 32-bit platform support: pass SIZEOF_DATUM=4 to enable (e.g., make SIZEOF_DATUM=4)
# When SIZEOF_DATUM=4, PASSEDBYVALUE is stripped from graphid type for pass-by-reference.
# If not specified, normal 64-bit behavior is used (PASSEDBYVALUE preserved).
Expand All @@ -228,16 +306,31 @@ src/include/parser/cypher_kwlist_d.h: src/include/parser/cypher_kwlist.h $(GEN_K

src/include/parser/cypher_gram_def.h: src/backend/parser/cypher_gram.c

src/backend/parser/cypher_gram.c: BISONFLAGS += --defines=src/include/parser/cypher_gram_def.h -Werror
#
# The Cypher grammar uses GLR mode with a number of inherent shift/reduce
# and reduce/reduce conflicts arising from the ambiguity between
# parenthesized expressions and graph patterns (both start with '(').
# GLR handles these correctly at runtime by forking at the conflict
# point; %dprec annotations resolve cases where both forks succeed.
#
# We keep -Werror so any unexpected Bison warning (unused rules, undeclared
# types, etc.) still fails the build; we downgrade only the two conflict
# categories to plain warnings via -Wno-error=. The exact conflict totals
# are pinned by %expect / %expect-rr in cypher_gram.y, which Bison treats
# as exact-match: any deviation fails the build and forces an audit of
# the new conflicts.
#
src/backend/parser/cypher_gram.c: BISONFLAGS += --defines=src/include/parser/cypher_gram_def.h -Werror -Wno-error=conflicts-sr -Wno-error=conflicts-rr

src/backend/parser/cypher_parser.o: src/backend/parser/cypher_gram.c src/include/parser/cypher_gram_def.h
src/backend/parser/cypher_parser.bc: src/backend/parser/cypher_gram.c src/include/parser/cypher_gram_def.h
src/backend/parser/cypher_keywords.o: src/backend/parser/cypher_gram.c src/include/parser/cypher_gram_def.h
src/backend/parser/cypher_keywords.bc: src/backend/parser/cypher_gram.c src/include/parser/cypher_gram_def.h
src/backend/parser/ag_scanner.c: FLEX_NO_BACKUP=yes

# Build the default install SQL (age--<VER>.sql) from current HEAD's sql/sql_files.
# This is what CREATE EXTENSION age installs — it contains ALL current functions.
# All 31 non-upgrade regression tests run against this complete SQL.
# Every non-upgrade regression test runs against this complete SQL.
$(age_sql): $(SQLS)
@echo "Building install SQL: $@ from HEAD"
@cat $(SQLS) > $@
Expand All @@ -246,6 +339,12 @@ ifeq ($(SIZEOF_DATUM),4)
@sed 's/^ PASSEDBYVALUE,$$/ -- PASSEDBYVALUE removed for 32-bit (see Makefile)/' $@ > $@.tmp && mv $@.tmp $@
endif

# ===== Upgrade regression-test support (2/2: rules + installcheck lifecycle) =====
#
# Part 1/2 (variables) is above the PGXS include; the rules and target
# hooks below must follow the include.
#
# --- Synthetic SQL rules ---
# Build synthetic "initial" version install SQL from the version-bump commit.
# This represents the pre-upgrade state — the SQL at the time the version was
# bumped in age.control. Used only by the upgrade test.
Expand All @@ -265,9 +364,7 @@ $(age_upgrade_test_sql): $(AGE_UPGRADE_TEMPLATE)
@sed -e "s/1\.X\.0/$(AGE_CURR_VER)/g" -e "s/y\.y\.y/$(AGE_CURR_VER)/g" $< > $@
endif

src/backend/parser/ag_scanner.c: FLEX_NO_BACKUP=yes

# --- Upgrade test file lifecycle during installcheck ---
# --- installcheck lifecycle: stage synthetic files, then clean up ---
#
# Problem: The upgrade test needs age--<INIT>.sql and age--<INIT>--<CURR>.sql
# in the PG extension directory for CREATE EXTENSION VERSION and ALTER
Expand All @@ -287,11 +384,68 @@ SHAREDIR = $(shell $(PG_CONFIG) --sharedir)
installcheck: export LC_COLLATE=C
ifneq ($(AGE_HAS_UPGRADE_TEST),)
.PHONY: _install_upgrade_test_files
_install_upgrade_test_files: $(age_init_sql) $(age_upgrade_test_sql) ## Build, install synthetic files, generate cleanup script
_install_upgrade_test_files: $(age_init_sql) $(age_upgrade_test_sql) # Build, install synthetic files, generate cleanup script
@echo "Installing upgrade test files to $(SHAREDIR)/extension/"
@$(INSTALL_DATA) $(age_init_sql) $(age_upgrade_test_sql) '$(SHAREDIR)/extension/'
@printf '#!/bin/sh\nrm -f "$(SHAREDIR)/extension/$(age_init_sql)" "$(SHAREDIR)/extension/$(age_upgrade_test_sql)"\nrm -f "$(age_init_sql)" "$(age_upgrade_test_sql)" "$(ag_regress_dir)/age_upgrade_cleanup.sh"\n' > $(ag_regress_dir)/age_upgrade_cleanup.sh
@chmod +x $(ag_regress_dir)/age_upgrade_cleanup.sh

installcheck: _install_upgrade_test_files
endif

# ===== installcheck-existing: run tests against a running server =====
#
# Runs the regression suite against an already-running PostgreSQL server
# instead of the private temp instance built by "make installcheck".
#
# "make installcheck" appends --temp-instance to REGRESS_OPTS, so it builds
# its own throwaway cluster and needs no running server. This target instead
# connects to the server selected by the standard libpq environment variables
# (PGHOST/PGPORT/PGUSER); PGDATABASE defaults to contrib_regression. Override
# any of them on the command line, e.g.:
#
# make installcheck-existing PGHOST=localhost PGPORT=5432 PGUSER=postgres
#
# pg_regress creates the database and loads the extension itself through
# --load-extension=age -- exactly as the temp-instance path does -- so no
# manual "CREATE EXTENSION" step is required. The connecting role must be
# allowed to CREATE DATABASE.
#
# This deliberately does NOT pass pg_regress --use-existing: that option skips
# database creation (which also disables --load-extension) and is only needed
# on clusters where the test role cannot CREATE DATABASE. For that narrow
# case, pre-create the database and extension and add --use-existing to
# EXTRA_REGRESS_OPTS.
#
# The upgrade test (age_upgrade) is excluded here: it installs synthetic
# extension files into the local $(SHAREDIR), which an existing or remote
# server would not see. Validate the upgrade path with "make installcheck".
#
# Locale note: locale-sensitive comparisons follow the existing server's own
# collation (fixed at its initdb time); the temp-instance locale flags do not
# apply to an already-running server.
PGDATABASE ?= contrib_regression
REGRESS_EXISTING = $(filter-out age_upgrade,$(REGRESS))

.PHONY: installcheck-existing
installcheck-existing:
$(pg_regress_installcheck) \
--inputdir=$(ag_regress_dir) \
--outputdir=$(ag_regress_dir) \
--load-extension=age \
$(if $(PGHOST),--host=$(PGHOST)) \
$(if $(PGPORT),--port=$(PGPORT)) \
$(if $(PGUSER),--user=$(PGUSER)) \
--dbname=$(PGDATABASE) \
$(REGRESS_EXISTING)

# ===== Help =====
.PHONY: help
help:
@echo "Apache AGE - common make targets:"
@echo " all Build the extension (default target)"
@echo " install Install the extension into the PostgreSQL tree"
@echo " installcheck Run the regression suite in a private temp instance"
@echo " installcheck-existing Run the regression suite against a running server"
@echo " clean Remove build artifacts"
@echo " help Show this message"
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,16 @@ LOAD 'age';
SET search_path = ag_catalog, "$user", public;
```

### Note on `ag_catalog` ownership

AGE installs all of its objects into the `ag_catalog` schema. Install AGE
(`CREATE EXTENSION age`) **before** granting the `CREATE` privilege on the
database to other roles. A role that can create schemas could otherwise
pre-create `ag_catalog` and own it; `CREATE EXTENSION age` therefore refuses to
install when `ag_catalog` already exists and is owned by a different role. If you
hit that error, drop the stray schema (`DROP SCHEMA ag_catalog CASCADE`) or
transfer its ownership to the installing role, then retry.

<h2><img height="20" src="/img/contents.svg">&nbsp;&nbsp;Using AGE with Non-Autocommit Clients (psycopg, JDBC, etc.)</h2>

If you are using AGE from a database client that does **not** default to autocommit — most commonly `psycopg` v3 or JDBC — you must understand how PostgreSQL's transaction semantics apply to AGE's setup and DDL-like functions. Otherwise, you may see graphs or labels that appear to be created successfully, but are not visible from new connections.
Expand Down
Loading
Loading