Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 22 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!-- PR title format: follow Conventional Commits style, e.g.
feat: add new postgres minor release images
fix: correctly mark stale instances as unknown
docs: add database migration guide
Common types: feat, fix, docs, chore, test, refactor, build(release)
See: https://www.conventionalcommits.org/en/v1.0.0/#summary
-->
## Summary

<!-- Briefly describe what changed and why. Link to a GitHub issue if appropriate. -->

## Checklist

- [ ] Tests added or updated (unit, regression, integration as needed)
- [ ] Docs/README updated (or not, with justification)
- [ ] Issue is linked (branch name or URL in PR description)
- [ ] Security checks (no secrets, vulnerabilities)
- [ ] Breaking changes (if any) are clearly called out in the PR description

## Notes for Reviewers

<!-- Instructions to help with the review including known limitations, design rationale, or follow-up tasks. -->
8 changes: 4 additions & 4 deletions .github/workflows/run_runner_test_harness.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ jobs:

steps:
- name: Checkout snowflake
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ github.ref }}

- name: Set up Docker
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2

- name: Build Docker Image
run: |
Expand All @@ -49,7 +49,7 @@ jobs:
exit 1

- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4
with:
python-version: '3.10'

Expand All @@ -68,7 +68,7 @@ jobs:
cat latest.log

- name: Upload Log File as Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: latest-log-${{ matrix.pgver }}
path: latest.log
6 changes: 3 additions & 3 deletions .github/workflows/snowflake_regression_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ github.ref }}

- name: Set up Docker
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2

- name: Build Docker Image
run: |
Expand Down Expand Up @@ -269,7 +269,7 @@ jobs:

- name: Upload Log File as Artifact
if: success() || failure()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: latest-log-${{ matrix.pgver }}
path: latest.log
Expand Down
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Snowflake Extension – Claude Guidelines

## Pull Requests

When creating a PR description, read and follow the template in
`.github/pull_request_template.md`. Use Conventional Commits style for
the PR title (e.g. `feat:`, `fix:`, `docs:`, `chore:`, `test:`, `refactor:`).
10 changes: 4 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
ARG PGVER
FROM postgres:$PGVER-alpine
ARG PGVER=17
FROM postgres:${PGVER}-alpine

RUN apk add --no-cache \
make \
gcc \
musl-dev \
postgresql-dev \
git \
clang \
llvm
git

WORKDIR /home/postgres/snowflake

COPY . /home/postgres/snowflake/

RUN USE_PGXS=1 make && USE_PGXS=1 make install
RUN USE_PGXS=1 make with_llvm=no && USE_PGXS=1 make with_llvm=no install

EXPOSE 5432

Expand Down
19 changes: 13 additions & 6 deletions test/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,9 @@ def runTest(testName):
try:
# Build the psql command with appropriate switches/inputs/outputs,
psql_command, actual_output_file, expected_output_file = build_psql_command(testName)
# Execute the psql command
result = subprocess.run(psql_command, shell=True, capture_output=True, text=True)
# Execute the psql command, redirecting output to file via Python (not shell)
with open(actual_output_file, 'w') as outf:
result = subprocess.run(psql_command, stdout=outf, stderr=subprocess.STDOUT, text=True)
Comment on lines +194 to +196
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Initialize result before the failing calls.

Line 195 can fail before subprocess.run(...) assigns result (build_psql_command(...) and open(...) can both raise). When that happens, the except path tries to write to result.stderr / result.returncode and masks the real failure with UnboundLocalError.

🔧 Suggested fix
     if extension == '.sql':
+        result = subprocess.CompletedProcess([], 1, "", "")
         # Execute psql command and Compare expected/actual output files, handle any errors gracefully through try/except block
         try:
             # Build the psql command with appropriate switches/inputs/outputs,
             psql_command, actual_output_file, expected_output_file = build_psql_command(testName)
             # Execute the psql command, redirecting output to file via Python (not shell)
             with open(actual_output_file, 'w') as outf:
                 result = subprocess.run(psql_command, stdout=outf, stderr=subprocess.STDOUT, text=True)
...
         except Exception as e:
             # Capture the error message
             result_status = "error"
-            result.stderr = f"An unexpected error occurred: {str(e)}"
-            result.returncode = 1  # Set return code to 1 for error
+            result = subprocess.CompletedProcess(
+                psql_command if 'psql_command' in locals() else [testName],
+                1,
+                "",
+                f"An unexpected error occurred: {e}",
+            )
🧰 Tools
🪛 Ruff (0.15.5)

[error] 196-196: subprocess call: check for execution of untrusted input

(S603)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/runner.py` around lines 194 - 196, The exception handler can reference
result.stderr/result.returncode even if result was never assigned (errors from
build_psql_command(...) or open(actual_output_file, 'w') can occur), so
initialize result to a safe default before those calls; e.g., set result = a
placeholder object or None and update the except block to handle that
placeholder when checking result.stderr/result.returncode, ensuring references
to result in the except path cannot raise UnboundLocalError (affecting the
subprocess.run invocation and its error handling).

# Compare actual and expected output files, setting shallow=False compares content and not just timestamps/size
if filecmp.cmp(actual_output_file, expected_output_file, shallow=False):
result_status = "pass"
Expand Down Expand Up @@ -325,16 +326,22 @@ def build_psql_command(sql_file):
# Assume the expected output file is in the same location as the sql_file, but with a .out extension
expected_output_file = sql_file.replace('.sql', '.out')

# Construct the full psql command using the database parameters and file paths
psql_command = f"{psql_command_path} -X -a -d {pgdb} -p {port} -h {pghost} < {sql_file} > {actual_output_file} 2>&1"

# Construct the psql command as an argument list (no shell interpretation needed)
psql_command = [
psql_command_path, "-X", "-a",
"-d", pgdb,
"-p", str(port),
"-h", pghost,
"-f", sql_file,
]

if glDebug:
print("sql_file: " + str(sql_file))
print("port: " + str(port))
print("actual output file: " + str(actual_output_file))
print("expected output file: " + str(expected_output_file))
print("psql_command: " + str(psql_command))

# Return the constructed psql command and the paths for the actual and expected output files
return psql_command, actual_output_file, expected_output_file

Expand Down
46 changes: 46 additions & 0 deletions test/t/lib/18config.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Use this file to set a group of values to environment variables; you can source this file to set all the values at once.
export EDGE_INSTALL_SCRIPT=install.py
export REPO=https://pgedge-devel.s3.amazonaws.com/REPO
export EDGE_REPO=$REPO/$EDGE_INSTALL_SCRIPT
export EDGE_HOST=127.0.0.1

# Use this environment variable to set the number of seconds that a timing-sensitive test
# sleeps before confirming a result has been replicated.
export EDGE_SLEEP=5

# Your setup scripts should start at the following port, and iterate through the setup for the number of nodes in
# EDGE_NODES.

export EDGE_START_PORT=5432
export EDGE_NODES=2

# This is where the installation should happen:
export NC_DIR="nc"
export EDGE_HOME_DIR="$NC_DIR/pgedge"
export EDGE_CLUSTER="demo"
export EDGE_CLUSTER_DIR="$EDGE_HOME_DIR/cluster/$EDGE_CLUSTER"

# These are the arguments associated with the cli setup:
export EDGE_USERNAME="lcusr"
export EDGE_PASSWORD="password"
export EDGE_DB="lcdb"
export EDGE_REPUSER=`whoami`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Show the new variable plus the two loader implementations that read these env files.
rg -n -C2 'EDGE_REPUSER|load_dotenv|def source_config_file' test/t/lib/18config.env test/t/util_test.py test/runner.py

Repository: pgEdge/snowflake

Length of output: 1670


🏁 Script executed:

# Read the source_config_file implementation in test/runner.py (lines 424-450)
sed -n '424,450p' test/runner.py

Repository: pgEdge/snowflake

Length of output: 1654


🏁 Script executed:

# Read the source_config_file implementations in test/t/util_test.py (around lines 146-170 and 199-220)
sed -n '146,220p' test/t/util_test.py

Repository: pgEdge/snowflake

Length of output: 4262


🏁 Script executed:

# Check if 18config.env file is referenced anywhere in the codebase
rg -n '18config\.env' --type py --type sh

Repository: pgEdge/snowflake

Length of output: 42


🏁 Script executed:

# Search for any references to 18config or this specific env file
rg -i '18config|EDGE_REPUSER' --type py --type sh --type yaml --type toml --type json

Repository: pgEdge/snowflake

Length of output: 42


🏁 Script executed:

# Check if 18config.env exists and is new/recent
ls -la test/t/lib/18config.env 2>/dev/null || echo "File not found"

Repository: pgEdge/snowflake

Length of output: 132


🏁 Script executed:

# Search broader patterns to see if this env file is referenced in any way
rg -i 'lib.*env|config\.env' test/ --type py --type sh | head -20

Repository: pgEdge/snowflake

Length of output: 257


🏁 Script executed:

# Check what's in the config.env file that IS being used
cat test/t/lib/config.env 2>/dev/null | head -30

Repository: pgEdge/snowflake

Length of output: 42


🏁 Script executed:

# See if 18config.env is sourced or loaded anywhere
rg -n 'source.*config|load_dotenv' test/

Repository: pgEdge/snowflake

Length of output: 1101


🏁 Script executed:

# Check what config_file is being passed to source_config_file in test/runner.py
sed -n '120,130p' test/runner.py

Repository: pgEdge/snowflake

Length of output: 478


🏁 Script executed:

# Check the context around line 125 in test/runner.py to see which file is loaded
sed -n '110,130p' test/runner.py

Repository: pgEdge/snowflake

Length of output: 1180


🏁 Script executed:

# Check if there are any test files that reference config options or 18config
find test/ -name "*.py" -o -name "*.sh" | xargs grep -l "18config\|EDGE_REPUSER" 2>/dev/null || echo "No references found"

Repository: pgEdge/snowflake

Length of output: 79


🏁 Script executed:

# Check the git log or PR context to understand if this is a new file being added
git log --oneline test/t/lib/18config.env 2>/dev/null | head -5 || echo "File may be newly added"

Repository: pgEdge/snowflake

Length of output: 105


This new config file is never imported or sourced anywhere in the codebase, and the backtick syntax will not be evaluated by the loaders.

The file test/t/lib/18config.env appears to be orphaned—there are no references to it in any test scripts or configuration. Additionally, if this file is ever used via the -c option in test/runner.py or loaded through the python-dotenv library, the backticks on line 27 will not be evaluated; they will become the literal string `whoami`, breaking any test expecting the current username.

The loaders (os.path.expandvars() and python-dotenv) only expand $VAR or ${VAR} syntax, not shell command substitution.

Fix: Use ${USER} instead of backticks, and verify this file is actually integrated into the test suite:

-export EDGE_REPUSER=`whoami`
+export EDGE_REPUSER="${USER}"

Also confirm whether 18config.env is intended to replace config.env or serve a different purpose, since it is not currently being loaded by any test infrastructure.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export EDGE_REPUSER=`whoami`
export EDGE_REPUSER="${USER}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/t/lib/18config.env` at line 27, The file test/t/lib/18config.env defines
EDGE_REPUSER using backticks which won't be evaluated by loaders; change the
assignment to use shell-expansion-friendly syntax (e.g., EDGE_REPUSER=${USER})
and remove backticks, then ensure this file is actually loaded by the test infra
(add it to whatever config list or import in test/runner.py or load via
python-dotenv/os.path.expandvars where needed) or drop/rename it if it's
redundant; also verify whether 18config.env is intended to replace or supplement
config.env and update references accordingly so tests receive the correct
username value.


# postgres version details
export EDGE_INST_VERSION=18
export EDGE_COMPONENT="pg$EDGE_INST_VERSION"

# Leave spock_ver empty if you want to use spocks default version
# As of 1st August 2024, spock40 is the default pinned version

# As of 10/23/24: Note that if the spock version is empty, cluster add-node will FAIL:
# It will return an error: ERROR: function spock.set_cluster_readonly() does not exist
export EDGE_SPOCK_VER="4.0.9"

export EDGE_CLI="pgedge"

# Path to store autoddl related actual outputs
export EDGE_ACTUAL_OUT_DIR="/tmp/auto_ddl/"

# To ensure locale related outputs (such as monetary values) stay consistent
export LC_ALL="C.UTF-8"
10 changes: 5 additions & 5 deletions test/t/util_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import sys, os, psycopg2, json, subprocess, shutil, re, csv, socket
import sys, os, psycopg2, json, subprocess, shutil, re, csv, socket, shlex
from dotenv import load_dotenv
from psycopg2 import sql

Expand Down Expand Up @@ -85,7 +85,7 @@ def enable_autoddl(host, dbname, port, pw, usr):

def run_cmd(msg, cmd, node_path):
print(cmd)
result = subprocess.run(f"{node_path}/pgedge/pgedge {cmd}", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
result = subprocess.run([f"{node_path}/pgedge/pgedge", *shlex.split(cmd)], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) # nosec B603
return result

# **************************************************************************************************************
Expand All @@ -97,7 +97,7 @@ def run_cmd(msg, cmd, node_path):

def run_nc_cmd(msg, cmd, node_path):
print(cmd)
result = subprocess.run(f"{node_path}/pgedge {cmd}", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
result = subprocess.run([f"{node_path}/pgedge", *shlex.split(cmd)], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) # nosec B603
return result

# **************************************************************************************************************
Expand Down Expand Up @@ -358,9 +358,9 @@ def get_sqlite_connection(db_file):

## Execute a query on the SQLite database:

def execute_sqlite_query(conn):
def execute_sqlite_query(conn, query):
cur = conn.cursor()
cur.execute(f"{query}")
cur.execute(query)
rows = cur.fetchall()
for row in rows:
print(row)
Expand Down
Loading