# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# 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

# ===== 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:
#
#   1. Builds the default install SQL (age--<CURR>.sql) from current HEAD's
#      sql/sql_files. This is what CREATE EXTENSION age installs.
#   2. Builds a synthetic "initial" version install SQL from the version-bump
#      commit in git history. This represents the pre-upgrade state.
#   3. Stamps the upgrade template to upgrade from the initial version to the
#      current version.
#   4. Temporarily installs the synthetic files into the PG extension directory
#      so that CREATE EXTENSION age VERSION '<INIT>' and ALTER EXTENSION
#      age UPDATE TO '<CURR>' can find them.
#   5. The age_upgrade regression test snapshots the ag_catalog schema from
#      a fresh install, then installs at INIT, upgrades to CURR, and compares
#      the catalog across seven system catalogs to detect missing, extra,
#      or changed objects.
#   6. The test SQL cleans up the synthetic files via a generated shell script.
#
# This forces developers to keep the upgrade template in sync: any SQL object
# added after the version-bump commit must also appear in the template, or the
# upgrade test will fail (the object will be missing after ALTER EXTENSION UPDATE).
#
# Because the default install SQL comes from HEAD, all non-upgrade tests
# run with every SQL function registered — no functions are missing.
#
# Graceful degradation — the upgrade test is silently skipped when:
#   - No git history (tarball build): AGE_VER_COMMIT is empty.
#   - No upgrade template: age--<CURR>--y.y.y.sql does not exist.
#   - A real (git-tracked) upgrade script from <CURR> already exists
#     (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)
# 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
AGE_INIT_VER := $(AGE_CURR_VER)_initial
# The upgrade template file (e.g., age--1.7.0--y.y.y.sql); empty if not present
AGE_UPGRADE_TEMPLATE := $(wildcard age--$(AGE_CURR_VER)--y.y.y.sql)

# Synthetic filenames — these are NOT installed permanently; they are temporarily
# placed in $(SHAREDIR)/extension/ during installcheck and removed after.
age_init_sql = $(if $(AGE_INIT_VER),age--$(AGE_INIT_VER).sql)
age_upgrade_test_sql = $(if $(AGE_INIT_VER),age--$(AGE_INIT_VER)--$(AGE_CURR_VER).sql)

# Real (git-tracked, non-template) upgrade scripts FROM the current version.
# If any exist (e.g., age--1.7.0--1.8.0.sql is committed), the synthetic
# upgrade test is redundant because a real upgrade path already ships.
# Uses git ls-files so untracked synthetic files are NOT matched.
AGE_REAL_UPGRADE := $(shell git ls-files 'age--$(AGE_CURR_VER)--*.sql' 2>/dev/null | grep -v 'y\.y\.y')

# Non-empty when ALL of these hold:
#   1. Git history is available (AGE_VER_COMMIT non-empty)
#   2. The upgrade template exists (AGE_UPGRADE_TEMPLATE non-empty)
#   3. No real upgrade script from current version exists (AGE_REAL_UPGRADE empty)
# When a real upgrade script ships, the test is skipped — the real script
# 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 \
       src/backend/catalog/ag_label.o \
       src/backend/catalog/ag_namespace.o \
       src/backend/commands/graph_commands.o \
       src/backend/commands/label_commands.o \
       src/backend/executor/cypher_create.o \
       src/backend/executor/cypher_merge.o \
       src/backend/executor/cypher_set.o \
       src/backend/executor/cypher_utils.o \
       src/backend/nodes/ag_nodes.o \
       src/backend/nodes/cypher_copyfuncs.o \
       src/backend/nodes/cypher_outfuncs.o \
       src/backend/nodes/cypher_readfuncs.o \
       src/backend/optimizer/cypher_createplan.o \
       src/backend/optimizer/cypher_pathnode.o \
       src/backend/optimizer/cypher_paths.o \
       src/backend/parser/ag_scanner.o \
       src/backend/parser/cypher_analyze.o \
       src/backend/parser/cypher_clause.o \
       src/backend/executor/cypher_delete.o \
       src/backend/parser/cypher_expr.o \
       src/backend/parser/cypher_gram.o \
       src/backend/parser/cypher_item.o \
       src/backend/parser/cypher_keywords.o \
       src/backend/parser/cypher_parse_agg.o \
       src/backend/parser/cypher_parse_node.o \
       src/backend/parser/cypher_parser.o \
       src/backend/parser/cypher_transform_entity.o \
       src/backend/utils/adt/age_graphid_ds.o \
       src/backend/utils/adt/agtype.o \
       src/backend/utils/adt/agtype_ext.o \
       src/backend/utils/adt/agtype_gin.o \
       src/backend/utils/adt/agtype_ops.o \
       src/backend/utils/adt/agtype_parser.o \
       src/backend/utils/adt/agtype_util.o \
       src/backend/utils/adt/agtype_raw.o \
       src/backend/utils/adt/age_global_graph.o \
       src/backend/utils/adt/age_session_info.o \
       src/backend/utils/adt/age_vle.o \
       src/backend/utils/adt/cypher_funcs.o \
       src/backend/utils/adt/ag_float8_supp.o \
       src/backend/utils/adt/graphid.o \
       src/backend/utils/ag_func.o \
       src/backend/utils/graph_generation.o \
       src/backend/utils/cache/ag_cache.o \
       src/backend/utils/cache/agehash.o \
       src/backend/utils/load/ag_load_labels.o \
       src/backend/utils/load/ag_load_edges.o \
       src/backend/utils/load/age_load.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
all_age_sql = $(shell find . -maxdepth 1 -type f -regex './age--[0-9]+\.[0-9]+\.[0-9]+\.sql')

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))

# ===== Regression test suite =====
# sorted in dependency order
REGRESS = scan \
          graphid \
          agtype \
          agtype_hash_cmp \
          agehash \
          catalog \
          cypher \
          expr \
          cypher_create \
          cypher_match \
          cypher_unwind \
          cypher_set \
          cypher_remove \
          cypher_delete \
          cypher_with \
          cypher_vle \
          age_shortest_path \
          cypher_union \
          cypher_call \
          cypher_merge \
          cypher_subquery \
          age_global_graph \
          age_load \
          index \
          analyze \
          graph_generation \
          name_validation \
          jsonb_operators \
          list_comprehension \
          predicate_functions \
          pattern_expression \
          age_reduce \
          map_projection \
          direct_field_access \
          generated_columns \
          security \
          reserved_keyword_alias \
          agtype_jsonb_cast \
          containment_selectivity \
          subgraph \
          extension_security

ifneq ($(EXTRA_TESTS),)
  REGRESS += $(EXTRA_TESTS)
endif

# Extension upgrade test — included when git history is available, the upgrade
# template exists, and no real upgrade script from the current version is
# committed. Runs between "security" and "drop" in test order.
ifneq ($(AGE_HAS_UPGRADE_TEST),)
  REGRESS += age_upgrade
endif

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

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 \
              $(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).

src/backend/parser/cypher_keywords.o: src/include/parser/cypher_kwlist_d.h

src/include/parser/cypher_kwlist_d.h: src/include/parser/cypher_kwlist.h $(GEN_KEYWORDLIST_DEPS)
	$(GEN_KEYWORDLIST) --extern --varname CypherKeyword --output src/include/parser $<

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

#
# 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.
# Every non-upgrade regression test runs against this complete SQL.
$(age_sql): $(SQLS)
	@echo "Building install SQL: $@ from HEAD"
	@cat $(SQLS) > $@
ifeq ($(SIZEOF_DATUM),4)
	@echo "32-bit build: removing PASSEDBYVALUE from graphid type"
	@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.
ifneq ($(AGE_HAS_UPGRADE_TEST),)
$(age_init_sql): age.control
	@echo "Building synthetic initial version install SQL: $@ from commit $(AGE_VER_COMMIT)"
	@for f in $$(git show $(AGE_VER_COMMIT):sql/sql_files 2>/dev/null); do \
		git show $(AGE_VER_COMMIT):sql/$${f}.sql 2>/dev/null; \
	done > $@
ifeq ($(SIZEOF_DATUM),4)
	@sed 's/^  PASSEDBYVALUE,$$/  -- PASSEDBYVALUE removed for 32-bit (see Makefile)/' $@ > $@.tmp && mv $@.tmp $@
endif

# Stamp upgrade template as upgrade from initial to current version
$(age_upgrade_test_sql): $(AGE_UPGRADE_TEMPLATE)
	@echo "Stamping upgrade template: $< -> $@ ($(AGE_INIT_VER) -> $(AGE_CURR_VER))"
	@sed -e "s/1\.X\.0/$(AGE_CURR_VER)/g" -e "s/y\.y\.y/$(AGE_CURR_VER)/g" $< > $@
endif

# --- 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
# EXTENSION UPDATE to find them, but we must not leave them installed
# permanently (they would confuse users).
#
# Solution: A Make prerequisite installs them before pg_regress runs, and the
# test SQL removes them at the end via \! (shell escape in psql). A generated
# cleanup script (regress/age_upgrade_cleanup.sh) contains the exact absolute
# paths so the removal works regardless of the working directory. EXTRA_CLEAN
# also removes them on "make clean" as a safety net.
#
# This adds a prerequisite to "installcheck" but does NOT override the PGXS
# recipe, so there are no Makefile warnings.
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
	@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"
