diff --git a/.github/workflows/ci-mariadb.yml b/.github/workflows/ci-mariadb.yml new file mode 100644 index 00000000..40c2555e --- /dev/null +++ b/.github/workflows/ci-mariadb.yml @@ -0,0 +1,117 @@ +name: CI MariaDB + +on: + push: + pull_request: + schedule: + # Weekly on Monday at 3:07 AM UTC + - cron: '7 3 * * 1' + workflow_dispatch: + +jobs: + employees-mariadb: + name: Employees DB (${{ matrix.mariadb-version }}) + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + mariadb-version: + - '10.11.9' + - '11.4.5' + - '12.1.2' + env: + SANDBOX_BINARY: ${{ github.workspace }}/opt/mysql + MARIADB_VERSION: ${{ matrix.mariadb-version }} + + steps: + - uses: actions/checkout@v4 + + - name: Install system libraries + run: | + sudo apt-get update + sudo apt-get install -y libaio1 libnuma1 libncurses5 + + - name: Install dbdeployer + run: | + curl -s https://raw.githubusercontent.com/ProxySQL/dbdeployer/master/scripts/dbdeployer-install.sh | bash + sudo mv dbdeployer /usr/local/bin/dbdeployer + + - name: Cache MariaDB tarball + uses: actions/cache@v4 + with: + path: /tmp/mariadb-tarball + key: mariadb-${{ matrix.mariadb-version }}-linux-x86_64-v1 + + - name: Download MariaDB + run: | + TARBALL="mariadb-${MARIADB_VERSION}-linux-systemd-x86_64.tar.gz" + URL="https://archive.mariadb.org/mariadb-${MARIADB_VERSION}/bintar-linux-systemd-x86_64/${TARBALL}" + mkdir -p /tmp/mariadb-tarball + if [ ! -f "/tmp/mariadb-tarball/$TARBALL" ]; then + echo "Downloading MariaDB ${MARIADB_VERSION}..." + curl -L -f -o "/tmp/mariadb-tarball/$TARBALL" "$URL" + fi + ls -lh "/tmp/mariadb-tarball/$TARBALL" + + - name: Unpack MariaDB + run: | + mkdir -p "$SANDBOX_BINARY" + TARBALL="mariadb-${MARIADB_VERSION}-linux-systemd-x86_64.tar.gz" + dbdeployer unpack "/tmp/mariadb-tarball/$TARBALL" \ + --sandbox-binary="$SANDBOX_BINARY" + + - name: Deploy sandbox + run: | + dbdeployer deploy single "$MARIADB_VERSION" \ + --sandbox-binary="$SANDBOX_BINARY" + + - name: Load employees database + run: | + ~/sandboxes/msb_*/use < employees.sql + + - name: Test MD5 integrity + run: | + ~/sandboxes/msb_*/use -t < test_employees_md5.sql > /tmp/test_md5.txt + cat /tmp/test_md5.txt + md5_ok=$(grep -iw ok /tmp/test_md5.txt | wc -l | tr -d ' \t') + if [ "$md5_ok" != "8" ]; then + echo "MD5 FAIL - expected 8 OK - found $md5_ok" + exit 1 + fi + echo "MD5 OK ($md5_ok matches)" + + - name: Test SHA integrity + run: | + ~/sandboxes/msb_*/use -t < test_employees_sha.sql > /tmp/test_sha.txt + cat /tmp/test_sha.txt + sha_ok=$(grep -iw ok /tmp/test_sha.txt | wc -l | tr -d ' \t') + if [ "$sha_ok" != "8" ]; then + echo "SHA FAIL - expected 8 OK - found $sha_ok" + exit 1 + fi + echo "SHA OK ($sha_ok matches)" + + - name: Load objects (stored procedures/functions) + run: | + ~/sandboxes/msb_*/use < objects.sql + + - name: Test stored procedures + run: | + SBDIR=$(ls -d ~/sandboxes/msb_*) + # Test functions + $SBDIR/use -BN -e "SELECT emp_name(10001);" employees + $SBDIR/use -BN -e "SELECT emp_dept_name(10001);" employees + $SBDIR/use -BN -e "SELECT current_manager('d001');" employees + # Test procedure + $SBDIR/use -t -e "CALL show_departments();" employees + + - name: Verify MariaDB version + run: | + ~/sandboxes/msb_*/use -BN -e "SELECT VERSION();" | grep -i mariadb + echo "OK: MariaDB detected" + + - name: Cleanup + if: always() + run: | + dbdeployer delete all --skip-confirm 2>/dev/null || true + pkill -9 -u "$USER" mysqld 2>/dev/null || true diff --git a/.github/workflows/ci-mysql.yml b/.github/workflows/ci-mysql.yml new file mode 100644 index 00000000..05b65b4a --- /dev/null +++ b/.github/workflows/ci-mysql.yml @@ -0,0 +1,452 @@ +name: CI MySQL + +on: + push: + pull_request: + schedule: + # Weekly on Monday at 3:07 AM UTC + - cron: '7 3 * * 1' + workflow_dispatch: + +jobs: + employees-mysql: + name: Employees DB (${{ matrix.mysql-version }}) + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + mysql-version: + - '5.6.51' + - '5.7.44' + - '8.0.42' + - '8.4.8' + - '9.0.1' + - '9.2.0' + - '9.5.0' + - '9.6.0' + env: + SANDBOX_BINARY: ${{ github.workspace }}/opt/mysql + MYSQL_VERSION: ${{ matrix.mysql-version }} + + steps: + - uses: actions/checkout@v4 + + - name: Install system libraries + run: | + sudo apt-get update + sudo apt-get install -y libaio1 libnuma1 libncurses5 + + - name: Install dbdeployer + run: | + curl -s https://raw.githubusercontent.com/ProxySQL/dbdeployer/master/scripts/dbdeployer-install.sh | bash + sudo mv dbdeployer /usr/local/bin/dbdeployer + + - name: Cache MySQL tarball + uses: actions/cache@v4 + with: + path: /tmp/mysql-tarball + key: mysql-${{ matrix.mysql-version }}-linux-x86_64-v3 + + - name: Download MySQL + run: | + SHORT_VER="${MYSQL_VERSION%.*}" + mkdir -p /tmp/mysql-tarball + CACHED=$(ls /tmp/mysql-tarball/mysql-${MYSQL_VERSION}-linux-*.tar.* 2>/dev/null | head -1) + if [ -n "$CACHED" ]; then + echo "Using cached tarball: $CACHED" + ls -lh "$CACHED" + exit 0 + fi + # MySQL 5.x uses .tar.gz / glibc2.12; 8.x+ uses .tar.xz / glibc2.17; 9.6+ uses glibc2.28 + MAJOR=$(echo "$MYSQL_VERSION" | cut -d. -f1) + if [ "$MAJOR" -lt 8 ]; then + GLIBCS="glibc2.12" + EXT="tar.gz" + else + GLIBCS="glibc2.17 glibc2.28" + EXT="tar.xz" + fi + for GLIBC in $GLIBCS; do + TARBALL="mysql-${MYSQL_VERSION}-linux-${GLIBC}-x86_64.${EXT}" + URL="https://dev.mysql.com/get/Downloads/MySQL-${SHORT_VER}/${TARBALL}" + echo "Trying ${GLIBC} (${EXT})..." + if curl -L -f -o "/tmp/mysql-tarball/$TARBALL" "$URL"; then + ls -lh "/tmp/mysql-tarball/$TARBALL" + exit 0 + fi + done + echo "ERROR: Could not download MySQL ${MYSQL_VERSION}" + exit 1 + + - name: Unpack MySQL + run: | + mkdir -p "$SANDBOX_BINARY" + TARBALL=$(ls /tmp/mysql-tarball/mysql-${MYSQL_VERSION}-linux-*.tar.* | head -1) + dbdeployer unpack "$TARBALL" \ + --sandbox-binary="$SANDBOX_BINARY" + + - name: Deploy sandbox + run: | + dbdeployer deploy single "$MYSQL_VERSION" \ + --sandbox-binary="$SANDBOX_BINARY" + + - name: Detect MySQL features + id: features + run: | + # --commands flag only exists in MySQL 9.5+ (SOURCE default changed to FALSE) + MAJOR=$(echo "$MYSQL_VERSION" | cut -d. -f1) + MINOR=$(echo "$MYSQL_VERSION" | cut -d. -f2) + NEEDS_COMMANDS="false" + if [ "$MAJOR" -gt 9 ] || { [ "$MAJOR" -eq 9 ] && [ "$MINOR" -ge 5 ]; }; then + NEEDS_COMMANDS="true" + fi + # Check if MD5()/SHA() are available (both removed in MySQL 9.6+) + HAS_MD5="true" + HAS_SHA="true" + if [ "$MAJOR" -gt 9 ] || { [ "$MAJOR" -eq 9 ] && [ "$MINOR" -ge 6 ]; }; then + HAS_MD5="false" + HAS_SHA="false" + fi + echo "needs_commands=$NEEDS_COMMANDS" >> "$GITHUB_OUTPUT" + echo "has_md5=$HAS_MD5" >> "$GITHUB_OUTPUT" + echo "has_sha=$HAS_SHA" >> "$GITHUB_OUTPUT" + echo "MySQL $MYSQL_VERSION: needs_commands=$NEEDS_COMMANDS, has_md5=$HAS_MD5, has_sha=$HAS_SHA" + + - name: Load employees database + run: | + EXTRA_ARGS="" + [ "${{ steps.features.outputs.needs_commands }}" == "true" ] && EXTRA_ARGS="--commands" + ~/sandboxes/msb_*/use $EXTRA_ARGS < employees.sql + + - name: Test MD5 integrity + if: steps.features.outputs.has_md5 == 'true' + run: | + EXTRA_ARGS="" + [ "${{ steps.features.outputs.needs_commands }}" == "true" ] && EXTRA_ARGS="--commands" + ~/sandboxes/msb_*/use $EXTRA_ARGS -t < test_employees_md5.sql > /tmp/test_md5.txt + cat /tmp/test_md5.txt + md5_ok=$(grep -iw ok /tmp/test_md5.txt | wc -l | tr -d ' \t') + if [ "$md5_ok" != "8" ]; then + echo "MD5 FAIL - expected 8 OK - found $md5_ok" + exit 1 + fi + echo "MD5 OK ($md5_ok matches)" + + - name: Test SHA integrity + if: steps.features.outputs.has_sha == 'true' + run: | + EXTRA_ARGS="" + [ "${{ steps.features.outputs.needs_commands }}" == "true" ] && EXTRA_ARGS="--commands" + ~/sandboxes/msb_*/use $EXTRA_ARGS -t < test_employees_sha.sql > /tmp/test_sha.txt + cat /tmp/test_sha.txt + sha_ok=$(grep -iw ok /tmp/test_sha.txt | wc -l | tr -d ' \t') + if [ "$sha_ok" != "8" ]; then + echo "SHA FAIL - expected 8 OK - found $sha_ok" + exit 1 + fi + echo "SHA OK ($sha_ok matches)" + + - name: Test SHA2 integrity + run: | + EXTRA_ARGS="" + [ "${{ steps.features.outputs.needs_commands }}" == "true" ] && EXTRA_ARGS="--commands" + ~/sandboxes/msb_*/use $EXTRA_ARGS -t < test_employees_sha2.sql > /tmp/test_sha2.txt + cat /tmp/test_sha2.txt + sha2_ok=$(grep -iw ok /tmp/test_sha2.txt | wc -l | tr -d ' \t') + if [ "$sha2_ok" != "8" ]; then + echo "SHA2 FAIL - expected 8 OK - found $sha2_ok" + exit 1 + fi + echo "SHA2 OK ($sha2_ok matches)" + + - name: Load objects (stored procedures/functions) + run: | + EXTRA_ARGS="" + [ "${{ steps.features.outputs.needs_commands }}" == "true" ] && EXTRA_ARGS="--commands" + ~/sandboxes/msb_*/use $EXTRA_ARGS < objects.sql + + - name: Test stored procedures + run: | + SBDIR=$(ls -d ~/sandboxes/msb_*) + $SBDIR/use -BN -e "SELECT emp_name(10001);" employees + $SBDIR/use -BN -e "SELECT emp_dept_name(10001);" employees + $SBDIR/use -BN -e "SELECT current_manager('d001');" employees + $SBDIR/use -t -e "CALL show_departments();" employees + $SBDIR/use -BN -e "SELECT COUNT(*) FROM v_full_employees;" employees + $SBDIR/use -BN -e "SELECT COUNT(*) FROM v_full_departments;" employees + + - name: Cleanup + if: always() + run: | + dbdeployer delete all --skip-confirm 2>/dev/null || true + pkill -9 -u "$USER" mysqld 2>/dev/null || true + + partitioned-mysql: + name: Employees Partitioned (${{ matrix.mysql-version }}) + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + mysql-version: + - '5.6.51' + - '5.7.44' + - '8.0.42' + - '8.4.8' + - '9.0.1' + - '9.2.0' + - '9.5.0' + - '9.6.0' + env: + SANDBOX_BINARY: ${{ github.workspace }}/opt/mysql + MYSQL_VERSION: ${{ matrix.mysql-version }} + + steps: + - uses: actions/checkout@v4 + + - name: Install system libraries + run: | + sudo apt-get update + sudo apt-get install -y libaio1 libnuma1 libncurses5 + + - name: Install dbdeployer + run: | + curl -s https://raw.githubusercontent.com/ProxySQL/dbdeployer/master/scripts/dbdeployer-install.sh | bash + sudo mv dbdeployer /usr/local/bin/dbdeployer + + - name: Cache MySQL tarball + uses: actions/cache@v4 + with: + path: /tmp/mysql-tarball + key: mysql-${{ matrix.mysql-version }}-linux-x86_64-v3 + + - name: Download MySQL + run: | + SHORT_VER="${MYSQL_VERSION%.*}" + mkdir -p /tmp/mysql-tarball + CACHED=$(ls /tmp/mysql-tarball/mysql-${MYSQL_VERSION}-linux-*.tar.* 2>/dev/null | head -1) + if [ -n "$CACHED" ]; then + echo "Using cached tarball: $CACHED" + ls -lh "$CACHED" + exit 0 + fi + MAJOR=$(echo "$MYSQL_VERSION" | cut -d. -f1) + if [ "$MAJOR" -lt 8 ]; then + GLIBCS="glibc2.12" + EXT="tar.gz" + else + GLIBCS="glibc2.17 glibc2.28" + EXT="tar.xz" + fi + for GLIBC in $GLIBCS; do + TARBALL="mysql-${MYSQL_VERSION}-linux-${GLIBC}-x86_64.${EXT}" + URL="https://dev.mysql.com/get/Downloads/MySQL-${SHORT_VER}/${TARBALL}" + echo "Trying ${GLIBC}..." + if curl -L -f -o "/tmp/mysql-tarball/$TARBALL" "$URL"; then + ls -lh "/tmp/mysql-tarball/$TARBALL" + exit 0 + fi + done + echo "ERROR: Could not download MySQL ${MYSQL_VERSION}" + exit 1 + + - name: Unpack MySQL + run: | + mkdir -p "$SANDBOX_BINARY" + TARBALL=$(ls /tmp/mysql-tarball/mysql-${MYSQL_VERSION}-linux-*.tar.* | head -1) + dbdeployer unpack "$TARBALL" \ + --sandbox-binary="$SANDBOX_BINARY" + + - name: Deploy sandbox + run: | + dbdeployer deploy single "$MYSQL_VERSION" \ + --sandbox-binary="$SANDBOX_BINARY" + + - name: Detect MySQL features + id: features + run: | + MAJOR=$(echo "$MYSQL_VERSION" | cut -d. -f1) + MINOR=$(echo "$MYSQL_VERSION" | cut -d. -f2) + NEEDS_COMMANDS="false" + if [ "$MAJOR" -gt 9 ] || { [ "$MAJOR" -eq 9 ] && [ "$MINOR" -ge 5 ]; }; then + NEEDS_COMMANDS="true" + fi + HAS_MD5="true" + if [ "$MAJOR" -gt 9 ] || { [ "$MAJOR" -eq 9 ] && [ "$MINOR" -ge 6 ]; }; then + HAS_MD5="false" + fi + echo "needs_commands=$NEEDS_COMMANDS" >> "$GITHUB_OUTPUT" + echo "has_md5=$HAS_MD5" >> "$GITHUB_OUTPUT" + + - name: Load partitioned employees database + run: | + EXTRA_ARGS="" + [ "${{ steps.features.outputs.needs_commands }}" == "true" ] && EXTRA_ARGS="--commands" + ~/sandboxes/msb_*/use $EXTRA_ARGS < employees_partitioned.sql + + - name: Verify partitioning + run: | + SBDIR=$(ls -d ~/sandboxes/msb_*) + PART_COUNT=$($SBDIR/use -BN -e \ + "SELECT COUNT(*) FROM information_schema.partitions WHERE table_schema='employees' AND table_name='titles';" \ + employees) + echo "titles partitions: $PART_COUNT" + [ "$PART_COUNT" -ge 18 ] || { echo "FAIL: expected >= 18 partitions on titles"; exit 1; } + PART_COUNT=$($SBDIR/use -BN -e \ + "SELECT COUNT(*) FROM information_schema.partitions WHERE table_schema='employees' AND table_name='salaries';" \ + employees) + echo "salaries partitions: $PART_COUNT" + [ "$PART_COUNT" -ge 18 ] || { echo "FAIL: expected >= 18 partitions on salaries"; exit 1; } + + - name: Test MD5 integrity + if: steps.features.outputs.has_md5 == 'true' + run: | + EXTRA_ARGS="" + [ "${{ steps.features.outputs.needs_commands }}" == "true" ] && EXTRA_ARGS="--commands" + ~/sandboxes/msb_*/use $EXTRA_ARGS -t < test_employees_md5.sql > /tmp/test_md5.txt + cat /tmp/test_md5.txt + md5_ok=$(grep -iw ok /tmp/test_md5.txt | wc -l | tr -d ' \t') + if [ "$md5_ok" != "8" ]; then + echo "MD5 FAIL - expected 8 OK - found $md5_ok" + exit 1 + fi + echo "MD5 OK ($md5_ok matches)" + + - name: Test SHA2 integrity + run: | + EXTRA_ARGS="" + [ "${{ steps.features.outputs.needs_commands }}" == "true" ] && EXTRA_ARGS="--commands" + ~/sandboxes/msb_*/use $EXTRA_ARGS -t < test_employees_sha2.sql > /tmp/test_sha2.txt + cat /tmp/test_sha2.txt + sha2_ok=$(grep -iw ok /tmp/test_sha2.txt | wc -l | tr -d ' \t') + if [ "$sha2_ok" != "8" ]; then + echo "SHA2 FAIL - expected 8 OK - found $sha2_ok" + exit 1 + fi + echo "SHA2 OK ($sha2_ok matches)" + + - name: Cleanup + if: always() + run: | + dbdeployer delete all --skip-confirm 2>/dev/null || true + pkill -9 -u "$USER" mysqld 2>/dev/null || true + + sakila-mysql: + name: Sakila DB (${{ matrix.mysql-version }}) + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + mysql-version: + - '5.6.51' + - '5.7.44' + - '8.0.42' + - '8.4.8' + - '9.0.1' + - '9.2.0' + - '9.5.0' + - '9.6.0' + env: + SANDBOX_BINARY: ${{ github.workspace }}/opt/mysql + MYSQL_VERSION: ${{ matrix.mysql-version }} + + steps: + - uses: actions/checkout@v4 + + - name: Install system libraries + run: | + sudo apt-get update + sudo apt-get install -y libaio1 libnuma1 libncurses5 + + - name: Install dbdeployer + run: | + curl -s https://raw.githubusercontent.com/ProxySQL/dbdeployer/master/scripts/dbdeployer-install.sh | bash + sudo mv dbdeployer /usr/local/bin/dbdeployer + + - name: Cache MySQL tarball + uses: actions/cache@v4 + with: + path: /tmp/mysql-tarball + key: mysql-${{ matrix.mysql-version }}-linux-x86_64-v3 + + - name: Download MySQL + run: | + SHORT_VER="${MYSQL_VERSION%.*}" + mkdir -p /tmp/mysql-tarball + CACHED=$(ls /tmp/mysql-tarball/mysql-${MYSQL_VERSION}-linux-*.tar.* 2>/dev/null | head -1) + if [ -n "$CACHED" ]; then + echo "Using cached tarball: $CACHED" + ls -lh "$CACHED" + exit 0 + fi + MAJOR=$(echo "$MYSQL_VERSION" | cut -d. -f1) + if [ "$MAJOR" -lt 8 ]; then + GLIBCS="glibc2.12" + EXT="tar.gz" + else + GLIBCS="glibc2.17 glibc2.28" + EXT="tar.xz" + fi + for GLIBC in $GLIBCS; do + TARBALL="mysql-${MYSQL_VERSION}-linux-${GLIBC}-x86_64.${EXT}" + URL="https://dev.mysql.com/get/Downloads/MySQL-${SHORT_VER}/${TARBALL}" + echo "Trying ${GLIBC}..." + if curl -L -f -o "/tmp/mysql-tarball/$TARBALL" "$URL"; then + ls -lh "/tmp/mysql-tarball/$TARBALL" + exit 0 + fi + done + echo "ERROR: Could not download MySQL ${MYSQL_VERSION}" + exit 1 + + - name: Unpack MySQL + run: | + mkdir -p "$SANDBOX_BINARY" + TARBALL=$(ls /tmp/mysql-tarball/mysql-${MYSQL_VERSION}-linux-*.tar.* | head -1) + dbdeployer unpack "$TARBALL" \ + --sandbox-binary="$SANDBOX_BINARY" + + - name: Deploy sandbox + run: | + dbdeployer deploy single "$MYSQL_VERSION" \ + --sandbox-binary="$SANDBOX_BINARY" + + - name: Detect MySQL features + id: features + run: | + MAJOR=$(echo "$MYSQL_VERSION" | cut -d. -f1) + MINOR=$(echo "$MYSQL_VERSION" | cut -d. -f2) + NEEDS_COMMANDS="false" + if [ "$MAJOR" -gt 9 ] || { [ "$MAJOR" -eq 9 ] && [ "$MINOR" -ge 5 ]; }; then + NEEDS_COMMANDS="true" + fi + echo "needs_commands=$NEEDS_COMMANDS" >> "$GITHUB_OUTPUT" + echo "MySQL $MYSQL_VERSION: needs_commands=$NEEDS_COMMANDS" + + - name: Load Sakila schema + run: | + EXTRA_ARGS="" + [ "${{ steps.features.outputs.needs_commands }}" == "true" ] && EXTRA_ARGS="--commands" + ~/sandboxes/msb_*/use $EXTRA_ARGS < sakila/sakila-mv-schema.sql + + - name: Load Sakila data + run: | + EXTRA_ARGS="" + [ "${{ steps.features.outputs.needs_commands }}" == "true" ] && EXTRA_ARGS="--commands" + ~/sandboxes/msb_*/use $EXTRA_ARGS < sakila/sakila-mv-data.sql + + - name: Verify Sakila data + run: | + SBDIR=$(ls -d ~/sandboxes/msb_*) + for table in actor film customer rental payment staff store; do + COUNT=$($SBDIR/use -BN -e "SELECT COUNT(*) FROM $table;" sakila) + echo "$table: $COUNT rows" + [ "$COUNT" -gt 0 ] || { echo "FAIL: $table is empty"; exit 1; } + done + $SBDIR/use -BN -e "SELECT COUNT(*) FROM film_list;" sakila + $SBDIR/use -BN -e "SELECT COUNT(*) FROM customer_list;" sakila + + - name: Cleanup + if: always() + run: | + dbdeployer delete all --skip-confirm 2>/dev/null || true + pkill -9 -u "$USER" mysqld 2>/dev/null || true diff --git a/.github/workflows/ci-percona.yml b/.github/workflows/ci-percona.yml new file mode 100644 index 00000000..6c9c326a --- /dev/null +++ b/.github/workflows/ci-percona.yml @@ -0,0 +1,112 @@ +name: CI Percona + +on: + push: + pull_request: + schedule: + # Weekly on Monday at 3:07 AM UTC + - cron: '7 3 * * 1' + workflow_dispatch: + +jobs: + employees-percona: + name: Employees DB (${{ matrix.percona-version }}) + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + percona-version: + - '8.0.36-28' + - '8.4.2-2' + env: + SANDBOX_BINARY: ${{ github.workspace }}/opt/mysql + PERCONA_VERSION: ${{ matrix.percona-version }} + + steps: + - uses: actions/checkout@v4 + + - name: Install system libraries + run: | + sudo apt-get update + sudo apt-get install -y libaio1 libnuma1 libncurses5 + + - name: Install dbdeployer + run: | + curl -s https://raw.githubusercontent.com/ProxySQL/dbdeployer/master/scripts/dbdeployer-install.sh | bash + sudo mv dbdeployer /usr/local/bin/dbdeployer + + - name: Cache Percona tarball + uses: actions/cache@v4 + with: + path: /tmp/percona-tarball + key: percona-${{ matrix.percona-version }}-linux-x86_64-v1 + + - name: Download Percona Server + run: | + MAJOR_MINOR=$(echo "$PERCONA_VERSION" | grep -oP '^\d+\.\d+') + GLIBC="glibc2.35" + TARBALL="Percona-Server-${PERCONA_VERSION}-Linux.x86_64.${GLIBC}-minimal.tar.gz" + URL="https://downloads.percona.com/downloads/Percona-Server-${MAJOR_MINOR}/Percona-Server-${PERCONA_VERSION}/binary/tarball/${TARBALL}" + mkdir -p /tmp/percona-tarball + if [ ! -f "/tmp/percona-tarball/$TARBALL" ]; then + echo "Downloading Percona Server ${PERCONA_VERSION} (${GLIBC})..." + curl -L -f -o "/tmp/percona-tarball/$TARBALL" "$URL" || { + GLIBC="glibc2.17" + TARBALL="Percona-Server-${PERCONA_VERSION}-Linux.x86_64.${GLIBC}-minimal.tar.gz" + URL="https://downloads.percona.com/downloads/Percona-Server-${MAJOR_MINOR}/Percona-Server-${PERCONA_VERSION}/binary/tarball/${TARBALL}" + echo "Retrying with glibc2.17..." + curl -L -f -o "/tmp/percona-tarball/$TARBALL" "$URL" + } + fi + ls -lh "/tmp/percona-tarball/"*.tar.gz + + - name: Unpack Percona Server + run: | + mkdir -p "$SANDBOX_BINARY" + TARBALL=$(ls /tmp/percona-tarball/Percona-Server-*.tar.gz | head -1) + dbdeployer unpack "$TARBALL" --sandbox-binary="$SANDBOX_BINARY" + + - name: Deploy sandbox + run: | + VERSION=$(ls "$SANDBOX_BINARY" | head -1) + echo "Deploying Percona Server $VERSION..." + dbdeployer deploy single "$VERSION" --sandbox-binary="$SANDBOX_BINARY" + + - name: Load employees database + run: | + ~/sandboxes/msb_*/use < employees.sql + + - name: Test MD5 integrity + run: | + ~/sandboxes/msb_*/use -t < test_employees_md5.sql > /tmp/test_md5.txt + cat /tmp/test_md5.txt + md5_ok=$(grep -iw ok /tmp/test_md5.txt | wc -l | tr -d ' \t') + if [ "$md5_ok" != "8" ]; then + echo "MD5 FAIL - expected 8 OK - found $md5_ok" + exit 1 + fi + echo "MD5 OK ($md5_ok matches)" + + - name: Test SHA integrity + run: | + ~/sandboxes/msb_*/use -t < test_employees_sha.sql > /tmp/test_sha.txt + cat /tmp/test_sha.txt + sha_ok=$(grep -iw ok /tmp/test_sha.txt | wc -l | tr -d ' \t') + if [ "$sha_ok" != "8" ]; then + echo "SHA FAIL - expected 8 OK - found $sha_ok" + exit 1 + fi + echo "SHA OK ($sha_ok matches)" + + - name: Verify Percona Server version + run: | + VERSION=$(~/sandboxes/msb_*/use -BN -e "SELECT VERSION();") + echo "Server version: $VERSION" + echo "$VERSION" | grep -iq percona || echo "Note: version string does not contain 'percona'" + echo "OK: Server running" + + - name: Cleanup + if: always() + run: | + dbdeployer delete all --skip-confirm 2>/dev/null || true + pkill -9 -u "$USER" mysqld 2>/dev/null || true diff --git a/.github/workflows/ci-postgresql.yml b/.github/workflows/ci-postgresql.yml new file mode 100644 index 00000000..e4699966 --- /dev/null +++ b/.github/workflows/ci-postgresql.yml @@ -0,0 +1,128 @@ +name: CI PostgreSQL + +on: + push: + pull_request: + schedule: + # Weekly on Monday at 3:07 AM UTC + - cron: '7 3 * * 1' + workflow_dispatch: + +jobs: + employees-postgresql: + name: Employees DB (PostgreSQL ${{ matrix.pg-version }}) + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + pg-version: + - '16' + - '17' + env: + PG_VERSION: ${{ matrix.pg-version }} + + steps: + - uses: actions/checkout@v4 + + - name: Install dbdeployer + run: | + curl -s https://raw.githubusercontent.com/ProxySQL/dbdeployer/master/scripts/dbdeployer-install.sh | bash + sudo mv dbdeployer /usr/local/bin/dbdeployer + + - name: Install PostgreSQL + run: | + sudo apt-get update + sudo apt-get install -y curl ca-certificates + sudo install -d /usr/share/postgresql-common/pgdg + sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \ + --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc + sudo sh -c 'echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] \ + http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \ + > /etc/apt/sources.list.d/pgdg.list' + sudo apt-get update + sudo apt-get install -y postgresql-${PG_VERSION} postgresql-client-${PG_VERSION} + + - name: Set up PostgreSQL binaries for dbdeployer + run: | + sudo systemctl stop postgresql || true + PG_FULL=$(dpkg -s postgresql-${PG_VERSION} | grep '^Version:' | sed 's/Version: //' | cut -d'-' -f1) + echo "PostgreSQL version: ${PG_FULL}" + mkdir -p ~/opt/postgresql/${PG_FULL}/{bin,lib,share} + cp -a /usr/lib/postgresql/${PG_VERSION}/bin/. ~/opt/postgresql/${PG_FULL}/bin/ + cp -a /usr/lib/postgresql/${PG_VERSION}/lib/. ~/opt/postgresql/${PG_FULL}/lib/ + cp -a /usr/share/postgresql/${PG_VERSION}/. ~/opt/postgresql/${PG_FULL}/share/ + + - name: Deploy sandbox + run: | + PG_FULL=$(ls ~/opt/postgresql/ | head -1) + echo "Deploying PostgreSQL ${PG_FULL}..." + dbdeployer deploy postgresql "${PG_FULL}" + + - name: Verify sandbox + run: | + SBDIR=$(ls -d ~/sandboxes/pg_sandbox_*) + $SBDIR/use -c "SELECT version();" + echo "OK: PostgreSQL sandbox running" + + - name: Load employees database + run: | + SBDIR=$(ls -d ~/sandboxes/pg_sandbox_*) + export PSQL="$SBDIR/use" + bash postgresql/load_employees_db.sh + + - name: Test MD5 integrity + run: | + SBDIR=$(ls -d ~/sandboxes/pg_sandbox_*) + $SBDIR/use < postgresql/test_employees_md5.sql > /tmp/test_md5.txt 2>&1 + cat /tmp/test_md5.txt + md5_ok=$(grep -iw ok /tmp/test_md5.txt | wc -l | tr -d ' \t') + if [ "$md5_ok" != "8" ]; then + echo "MD5 FAIL - expected 8 OK - found $md5_ok" + exit 1 + fi + echo "MD5 OK ($md5_ok matches)" + + - name: Test SHA integrity + run: | + SBDIR=$(ls -d ~/sandboxes/pg_sandbox_*) + $SBDIR/use < postgresql/test_employees_sha.sql > /tmp/test_sha.txt 2>&1 + cat /tmp/test_sha.txt + sha_ok=$(grep -iw ok /tmp/test_sha.txt | wc -l | tr -d ' \t') + if [ "$sha_ok" != "8" ]; then + echo "SHA FAIL - expected 8 OK - found $sha_ok" + exit 1 + fi + echo "SHA OK ($sha_ok matches)" + + - name: Test SHA2 integrity + run: | + SBDIR=$(ls -d ~/sandboxes/pg_sandbox_*) + $SBDIR/use < postgresql/test_employees_sha2.sql > /tmp/test_sha2.txt 2>&1 + cat /tmp/test_sha2.txt + sha2_ok=$(grep -iw ok /tmp/test_sha2.txt | wc -l | tr -d ' \t') + if [ "$sha2_ok" != "8" ]; then + echo "SHA2 FAIL - expected 8 OK - found $sha2_ok" + exit 1 + fi + echo "SHA2 OK ($sha2_ok matches)" + + - name: Load objects (stored procedures/functions) + run: | + SBDIR=$(ls -d ~/sandboxes/pg_sandbox_*) + $SBDIR/use < postgresql/objects.sql + + - name: Test stored procedures + run: | + SBDIR=$(ls -d ~/sandboxes/pg_sandbox_*) + $SBDIR/use -d employees -t -c "SELECT emp_name(10001);" + $SBDIR/use -d employees -t -c "SELECT emp_dept_name(10001);" + $SBDIR/use -d employees -t -c "SELECT current_manager('d001');" + $SBDIR/use -d employees -c "SELECT * FROM show_departments();" + $SBDIR/use -d employees -t -c "SELECT COUNT(*) FROM v_full_employees;" + $SBDIR/use -d employees -t -c "SELECT COUNT(*) FROM v_full_departments;" + + - name: Cleanup + if: always() + run: | + dbdeployer delete all --skip-confirm 2>/dev/null || true + pkill -9 -u "$USER" postgres 2>/dev/null || true diff --git a/README.md b/README.md index d6b59b4c..d08ebb65 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,64 @@ This repository was migrated from [Launchpad](https://launchpad.net/test-db). See usage in the [MySQL docs](https://dev.mysql.com/doc/employee/en/index.html) +[![CI MySQL](https://github.com/datacharmer/test_db/actions/workflows/ci-mysql.yml/badge.svg)](https://github.com/datacharmer/test_db/actions/workflows/ci-mysql.yml) +[![CI Percona](https://github.com/datacharmer/test_db/actions/workflows/ci-percona.yml/badge.svg)](https://github.com/datacharmer/test_db/actions/workflows/ci-percona.yml) +[![CI MariaDB](https://github.com/datacharmer/test_db/actions/workflows/ci-mariadb.yml/badge.svg)](https://github.com/datacharmer/test_db/actions/workflows/ci-mariadb.yml) +[![CI PostgreSQL](https://github.com/datacharmer/test_db/actions/workflows/ci-postgresql.yml/badge.svg)](https://github.com/datacharmer/test_db/actions/workflows/ci-postgresql.yml) + + +## Tested Versions + +The database requires MySQL 5.0+ or PostgreSQL 12+. The following versions are tested in CI +using [ProxySQL/dbdeployer](https://github.com/ProxySQL/dbdeployer) on a weekly schedule: + +| Vendor | Versions | +|--------|----------| +| MySQL | 5.6, 5.7, 8.0, 8.4, 9.0, 9.2, 9.5, 9.6 | +| Percona Server | 8.0, 8.4 | +| MariaDB | 10.11, 11.4, 12.1 | +| PostgreSQL | 16, 17 | + +### MySQL 9.x Notes + +Starting with MySQL 9.5, the `SOURCE` command requires the `--commands` flag on the client: + + mysql --commands < employees.sql + +Starting with MySQL 9.6, the `MD5()` and `SHA()` functions have been removed from the server. +The integrity test files `test_employees_md5.sql` and `test_employees_sha.sql` will not work on 9.6+. +Use `test_employees_sha2.sql` instead, which uses `SHA2(..., 256)` and is compatible with all versions: + + mysql -t < test_employees_sha2.sql + +The SHA-256 checksums are identical across all supported MySQL, Percona, MariaDB, and PostgreSQL versions. + + +## Tested Versions + +The database requires MySQL 5.0+ or compatible server. The following versions are tested in CI +using [ProxySQL/dbdeployer](https://github.com/ProxySQL/dbdeployer) on a weekly schedule: + +| Vendor | Versions | +|--------|----------| +| MySQL | 5.6, 5.7, 8.0, 8.4, 9.0, 9.2, 9.5, 9.6 | +| Percona Server | 8.0, 8.4 | +| MariaDB | 10.11, 11.4, 12.1 | + +### MySQL 9.x Notes + +Starting with MySQL 9.5, the `SOURCE` command requires the `--commands` flag on the client: + + mysql --commands < employees.sql + +Starting with MySQL 9.6, the `MD5()` and `SHA()` functions have been removed from the server. +The integrity test files `test_employees_md5.sql` and `test_employees_sha.sql` will not work on 9.6+. +Use `test_employees_sha2.sql` instead, which uses `SHA2(..., 256)` and is compatible with all versions: + + mysql -t < test_employees_sha2.sql + +The SHA-256 checksums are identical across all supported MySQL, Percona, and MariaDB versions. + ## Where it comes from @@ -54,11 +112,11 @@ If you want to install with two large partitioned tables, run ## Testing the installation -After installing, you can run one of the following +After installing, you can run one of the following integrity tests: - mysql -t < test_employees_md5.sql - # OR - mysql -t < test_employees_sha.sql + mysql -t < test_employees_sha2.sql # SHA-256 (works on all versions including 9.6+) + mysql -t < test_employees_md5.sql # MD5 (MySQL 8.0–9.5 only) + mysql -t < test_employees_sha.sql # SHA-1 (MySQL 8.0–9.5 only) For example: @@ -100,6 +158,53 @@ For example: +--------------+---------------+-----------+ +## PostgreSQL Installation + +The database is also available for PostgreSQL 12+. The schema and data are identical +to the MySQL version. All files are in the `postgresql/` directory. + +### Differences from the MySQL version + +- **ENUM type**: MySQL `ENUM('M','F')` is replaced with `CHAR(1) CHECK (gender IN ('M','F'))` +- **Stored procedures**: MySQL's `delimiter //` syntax is replaced with PostgreSQL dollar-quoting (`$...$ LANGUAGE plpgsql`) +- **`show_departments()`**: Implemented as a function returning TABLE (use `SELECT * FROM show_departments();` instead of `CALL show_departments();`) +- **User variables**: MySQL's `@var := value` pattern is replaced with PL/pgSQL local variables +- **Integrity tests**: Use the same incremental hashing approach but via PL/pgSQL helper functions instead of MySQL user variables + +### Data integrity across databases + +The SHA-256 checksums are **identical** between MySQL and PostgreSQL. This is verified in CI: +the same expected values in `test_employees_sha2.sql` and `postgresql/test_employees_sha2.sql` +produce matching results on both databases. The MySQL version uses `SHA2(..., 256)` while +PostgreSQL uses `encode(digest(..., 'sha256'), 'hex')` from the `pgcrypto` extension. + +### Installation + +1. Download the repository +2. Install PostgreSQL (12+) +3. Run the loading script: + + cd postgresql + bash load_employees_db.sh + + To use a custom psql command (e.g., from a dbdeployer sandbox): + + PSQL=/path/to/psql bash load_employees_db.sh + +### Testing the PostgreSQL installation + + psql -d employees < postgresql/test_employees_sha2.sql # SHA-256 (recommended) + psql -d employees < postgresql/test_employees_md5.sql # MD5 + psql -d employees < postgresql/test_employees_sha.sql # SHA-1 (requires pgcrypto) + +### Optional: load stored procedures and functions + + psql -d employees < postgresql/objects.sql + +Available functions: `emp_name()`, `emp_dept_name()`, `emp_dept_id()`, `current_manager()`, +`show_departments()` (use `SELECT * FROM show_departments();`), `employees_help()`. + + ## DISCLAIMER To the best of my knowledge, this data is fabricated and diff --git a/postgresql/employees.sql b/postgresql/employees.sql new file mode 100644 index 00000000..2bd18864 --- /dev/null +++ b/postgresql/employees.sql @@ -0,0 +1,91 @@ +-- Sample employee database +-- PostgreSQL version +-- +-- Original data created by Fusheng Wang and Carlo Zaniolo +-- Current schema by Giuseppe Maxia +-- PostgreSQL port for test_db +-- +-- This work is licensed under the +-- Creative Commons Attribution-Share Alike 3.0 Unported License. + +DROP DATABASE IF EXISTS employees; +CREATE DATABASE employees; +\connect employees + +SELECT 'CREATING DATABASE STRUCTURE' AS info; + +DROP TABLE IF EXISTS dept_emp, + dept_manager, + titles, + salaries, + employees, + departments CASCADE; + +CREATE TABLE employees ( + emp_no INT NOT NULL, + birth_date DATE NOT NULL, + first_name VARCHAR(14) NOT NULL, + last_name VARCHAR(16) NOT NULL, + gender CHAR(1) NOT NULL CHECK (gender IN ('M','F')), + hire_date DATE NOT NULL, + PRIMARY KEY (emp_no) +); + +CREATE TABLE departments ( + dept_no CHAR(4) NOT NULL, + dept_name VARCHAR(40) NOT NULL, + PRIMARY KEY (dept_no), + UNIQUE (dept_name) +); + +CREATE TABLE dept_manager ( + emp_no INT NOT NULL, + dept_no CHAR(4) NOT NULL, + from_date DATE NOT NULL, + to_date DATE NOT NULL, + FOREIGN KEY (emp_no) REFERENCES employees (emp_no) ON DELETE CASCADE, + FOREIGN KEY (dept_no) REFERENCES departments (dept_no) ON DELETE CASCADE, + PRIMARY KEY (emp_no,dept_no) +); + +CREATE TABLE dept_emp ( + emp_no INT NOT NULL, + dept_no CHAR(4) NOT NULL, + from_date DATE NOT NULL, + to_date DATE NOT NULL, + FOREIGN KEY (emp_no) REFERENCES employees (emp_no) ON DELETE CASCADE, + FOREIGN KEY (dept_no) REFERENCES departments (dept_no) ON DELETE CASCADE, + PRIMARY KEY (emp_no,dept_no) +); + +CREATE TABLE titles ( + emp_no INT NOT NULL, + title VARCHAR(50) NOT NULL, + from_date DATE NOT NULL, + to_date DATE, + FOREIGN KEY (emp_no) REFERENCES employees (emp_no) ON DELETE CASCADE, + PRIMARY KEY (emp_no,title, from_date) +); + +CREATE TABLE salaries ( + emp_no INT NOT NULL, + salary INT NOT NULL, + from_date DATE NOT NULL, + to_date DATE NOT NULL, + FOREIGN KEY (emp_no) REFERENCES employees (emp_no) ON DELETE CASCADE, + PRIMARY KEY (emp_no, from_date) +); + +CREATE OR REPLACE VIEW dept_emp_latest_date AS + SELECT emp_no, MAX(from_date) AS from_date, MAX(to_date) AS to_date + FROM dept_emp + GROUP BY emp_no; + +-- shows only the current department for each employee +CREATE OR REPLACE VIEW current_dept_emp AS + SELECT l.emp_no, dept_no, l.from_date, l.to_date + FROM dept_emp d + INNER JOIN dept_emp_latest_date l + ON d.emp_no=l.emp_no AND d.from_date=l.from_date AND l.to_date = d.to_date; + +SELECT 'SCHEMA CREATED' AS info; diff --git a/postgresql/load_employees_db.sh b/postgresql/load_employees_db.sh new file mode 100755 index 00000000..22f2e025 --- /dev/null +++ b/postgresql/load_employees_db.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +DUMP_DIR="$(dirname "$SCRIPT_DIR")" +PSQL="${PSQL:-psql}" + +echo "Creating database schema..." +"$PSQL" -f "$SCRIPT_DIR/employees.sql" + +echo "LOADING departments" +sed 's/`//g' "$DUMP_DIR/load_departments.dump" | "$PSQL" -d employees -q + +echo "LOADING employees" +sed 's/`//g' "$DUMP_DIR/load_employees.dump" | "$PSQL" -d employees -q + +echo "LOADING dept_emp" +sed 's/`//g' "$DUMP_DIR/load_dept_emp.dump" | "$PSQL" -d employees -q + +echo "LOADING dept_manager" +sed 's/`//g' "$DUMP_DIR/load_dept_manager.dump" | "$PSQL" -d employees -q + +echo "LOADING titles" +sed 's/`//g' "$DUMP_DIR/load_titles.dump" | "$PSQL" -d employees -q + +echo "LOADING salaries" +sed 's/`//g' "$DUMP_DIR/load_salaries1.dump" | "$PSQL" -d employees -q +sed 's/`//g' "$DUMP_DIR/load_salaries2.dump" | "$PSQL" -d employees -q +sed 's/`//g' "$DUMP_DIR/load_salaries3.dump" | "$PSQL" -d employees -q + +echo "Done loading employees database." diff --git a/postgresql/objects.sql b/postgresql/objects.sql new file mode 100644 index 00000000..db07ed02 --- /dev/null +++ b/postgresql/objects.sql @@ -0,0 +1,217 @@ +-- Additional objects for employees database +-- PostgreSQL version (PL/pgSQL) + +\connect employees + +DROP FUNCTION IF EXISTS emp_dept_id(INT); +DROP FUNCTION IF EXISTS emp_dept_name(INT); +DROP FUNCTION IF EXISTS emp_name(INT); +DROP FUNCTION IF EXISTS current_manager(CHAR(4)); +DROP FUNCTION IF EXISTS employees_usage(); +DROP FUNCTION IF EXISTS show_departments(); +DROP PROCEDURE IF EXISTS employees_help(); + +-- +-- returns the department id of a given employee +-- +CREATE OR REPLACE FUNCTION emp_dept_id(employee_id INT) +RETURNS CHAR(4) +LANGUAGE plpgsql +STABLE +AS $$ +DECLARE + max_date DATE; + result CHAR(4); +BEGIN + SELECT MAX(from_date) INTO max_date + FROM dept_emp + WHERE emp_no = employee_id; + + SELECT dept_no INTO result + FROM dept_emp + WHERE emp_no = employee_id AND from_date = max_date + LIMIT 1; + + RETURN result; +END; +$$; + +-- +-- returns the department name of a given employee +-- +CREATE OR REPLACE FUNCTION emp_dept_name(employee_id INT) +RETURNS VARCHAR(40) +LANGUAGE plpgsql +STABLE +AS $$ +BEGIN + RETURN ( + SELECT dept_name + FROM departments + WHERE dept_no = emp_dept_id(employee_id) + ); +END; +$$; + +-- +-- returns the employee name of a given employee id +-- +CREATE OR REPLACE FUNCTION emp_name(employee_id INT) +RETURNS VARCHAR(32) +LANGUAGE plpgsql +STABLE +AS $$ +BEGIN + RETURN ( + SELECT concat(first_name, ' ', last_name) AS name + FROM employees + WHERE emp_no = employee_id + ); +END; +$$; + +-- +-- returns the manager of a department +-- choosing the most recent one +-- from the manager list +-- +CREATE OR REPLACE FUNCTION current_manager(dept_id CHAR(4)) +RETURNS VARCHAR(32) +LANGUAGE plpgsql +STABLE +AS $$ +DECLARE + max_date DATE; + result VARCHAR(32); +BEGIN + SELECT MAX(from_date) INTO max_date + FROM dept_manager + WHERE dept_no = dept_id; + + SELECT emp_name(emp_no) INTO result + FROM dept_manager + WHERE dept_no = dept_id AND from_date = max_date + LIMIT 1; + + RETURN result; +END; +$$; + +-- +-- selects the employee records with the +-- latest department +-- +CREATE OR REPLACE VIEW v_full_employees +AS +SELECT + emp_no, + first_name, last_name, + birth_date, gender, + hire_date, + emp_dept_name(emp_no) AS department +FROM + employees; + +-- +-- selects the department list with manager names +-- +CREATE OR REPLACE VIEW v_full_departments +AS +SELECT + dept_no, dept_name, current_manager(dept_no) AS manager +FROM + departments; + +-- +-- shows the departments with the number of employees +-- per department +-- +CREATE OR REPLACE FUNCTION show_departments() +RETURNS TABLE(dept_no CHAR(4), dept_name VARCHAR(40), manager VARCHAR(32), count BIGINT) +LANGUAGE plpgsql +AS $$ +BEGIN + DROP TABLE IF EXISTS department_max_date; + DROP TABLE IF EXISTS department_people; + + CREATE TEMPORARY TABLE department_max_date + ( + emp_no INT NOT NULL PRIMARY KEY, + dept_from_date DATE NOT NULL, + dept_to_date DATE NOT NULL + ); + + INSERT INTO department_max_date + SELECT + de.emp_no, MAX(de.from_date), MAX(de.to_date) + FROM + dept_emp de + GROUP BY + de.emp_no; + + CREATE TEMPORARY TABLE department_people + ( + emp_no INT NOT NULL, + dept_no CHAR(4) NOT NULL, + PRIMARY KEY (emp_no, dept_no) + ); + + INSERT INTO department_people + SELECT dmd.emp_no, de.dept_no + FROM + department_max_date dmd + INNER JOIN dept_emp de + ON dmd.dept_from_date=de.from_date + AND dmd.dept_to_date=de.to_date + AND dmd.emp_no=de.emp_no; + + RETURN QUERY + SELECT + v.dept_no, v.dept_name, v.manager::VARCHAR(32), COUNT(*)::BIGINT + FROM v_full_departments v + INNER JOIN department_people dp ON v.dept_no = dp.dept_no + GROUP BY v.dept_no, v.dept_name, v.manager; + + DROP TABLE department_max_date; + DROP TABLE department_people; +END; +$$; + +CREATE OR REPLACE FUNCTION employees_usage() +RETURNS TEXT +LANGUAGE plpgsql +IMMUTABLE +AS $$ +BEGIN + RETURN ' + == USAGE == + ==================== + + FUNCTION show_departments() + + shows the departments with the manager and + number of employees per department + (returns TABLE - use: SELECT * FROM show_departments();) + + FUNCTION current_manager (dept_id) + + Shows who is the manager of a given department + + FUNCTION emp_name (emp_id) + + Shows name and surname of a given employee + + FUNCTION emp_dept_id (emp_id) + + Shows the current department of given employee +'; +END; +$$; + +CREATE OR REPLACE PROCEDURE employees_help() +LANGUAGE plpgsql +AS $$ +BEGIN + SELECT employees_usage() AS info; +END; +$$; diff --git a/postgresql/test_employees_md5.sql b/postgresql/test_employees_md5.sql new file mode 100644 index 00000000..aa7aea10 --- /dev/null +++ b/postgresql/test_employees_md5.sql @@ -0,0 +1,105 @@ +-- Test employees database integrity using MD5 checksums +-- PostgreSQL version + +\connect employees + +SELECT 'TESTING INSTALLATION' AS info; + +DROP TABLE IF EXISTS expected_values, found_values; +CREATE TABLE expected_values ( + table_name VARCHAR(30) NOT NULL PRIMARY KEY, + recs INT NOT NULL, + crc_sha VARCHAR(100) NOT NULL, + crc_md5 VARCHAR(100) NOT NULL +); + + +CREATE TABLE found_values (LIKE expected_values); + +INSERT INTO expected_values VALUES +('employees', 300024,'4d4aa689914d8fd41db7e45c2168e7dcb9697359', + '4ec56ab5ba37218d187cf6ab09ce1aa1'), +('departments', 9,'4b315afa0e35ca6649df897b958345bcb3d2b764', + 'd1af5e170d2d1591d776d5638d71fc5f'), +('dept_manager', 24,'9687a7d6f93ca8847388a42a6d8d93982a841c6c', + '8720e2f0853ac9096b689c14664f847e'), +('dept_emp', 331603, 'd95ab9fe07df0865f592574b3b33b9c741d9fd1b', + 'ccf6fe516f990bdaa49713fc478701b7'), +('titles', 443308,'d12d5f746b88f07e69b9e36675b6067abb01b60e', + 'bfa016c472df68e70a03facafa1bc0a8'), +('salaries', 2844047,'b5a1785c27d75e33a4173aaa22ccf41ebd7d4a9f', + 'fd220654e95aea1b169624ffe3fca934'); +SELECT table_name, recs AS expected_records, crc_md5 AS expected_crc FROM expected_values; + +-- Helper function: computes incremental MD5 over rows returned by query. +-- The query must return a single column named "row_text". +-- This replicates MySQL's @crc := MD5(CONCAT_WS('#', @crc, col1, col2, ...)) +-- by using crc := md5(crc || '#' || row_text) for each row. +CREATE OR REPLACE FUNCTION compute_table_md5(p_query TEXT) +RETURNS TEXT +LANGUAGE plpgsql +AS $$ +DECLARE + crc TEXT := ''; + r RECORD; +BEGIN + FOR r IN EXECUTE p_query LOOP + crc := md5(crc || '#' || r.row_text); + END LOOP; + RETURN crc; +END; +$$; + +-- Compute checksums for each table +INSERT INTO found_values VALUES +('employees', + (SELECT COUNT(*) FROM employees), + (SELECT compute_table_md5($$SELECT CONCAT_WS('#', emp_no, birth_date, first_name, last_name, gender, hire_date) AS row_text FROM employees ORDER BY emp_no$$)), + (SELECT compute_table_md5($$SELECT CONCAT_WS('#', emp_no, birth_date, first_name, last_name, gender, hire_date) AS row_text FROM employees ORDER BY emp_no$$))), + +('departments', + (SELECT COUNT(*) FROM departments), + (SELECT compute_table_md5($$SELECT CONCAT_WS('#', dept_no, dept_name) AS row_text FROM departments ORDER BY dept_no$$)), + (SELECT compute_table_md5($$SELECT CONCAT_WS('#', dept_no, dept_name) AS row_text FROM departments ORDER BY dept_no$$))), + +('dept_manager', + (SELECT COUNT(*) FROM dept_manager), + (SELECT compute_table_md5($$SELECT CONCAT_WS('#', dept_no, emp_no, from_date, to_date) AS row_text FROM dept_manager ORDER BY dept_no, emp_no$$)), + (SELECT compute_table_md5($$SELECT CONCAT_WS('#', dept_no, emp_no, from_date, to_date) AS row_text FROM dept_manager ORDER BY dept_no, emp_no$$))), + +('dept_emp', + (SELECT COUNT(*) FROM dept_emp), + (SELECT compute_table_md5($$SELECT CONCAT_WS('#', dept_no, emp_no, from_date, to_date) AS row_text FROM dept_emp ORDER BY dept_no, emp_no$$)), + (SELECT compute_table_md5($$SELECT CONCAT_WS('#', dept_no, emp_no, from_date, to_date) AS row_text FROM dept_emp ORDER BY dept_no, emp_no$$))), + +('titles', + (SELECT COUNT(*) FROM titles), + (SELECT compute_table_md5($$SELECT CONCAT_WS('#', emp_no, title, from_date, to_date) AS row_text FROM titles ORDER BY emp_no, title, from_date$$)), + (SELECT compute_table_md5($$SELECT CONCAT_WS('#', emp_no, title, from_date, to_date) AS row_text FROM titles ORDER BY emp_no, title, from_date$$))), + +('salaries', + (SELECT COUNT(*) FROM salaries), + (SELECT compute_table_md5($$SELECT CONCAT_WS('#', emp_no, salary, from_date, to_date) AS row_text FROM salaries ORDER BY emp_no, from_date, to_date$$)), + (SELECT compute_table_md5($$SELECT CONCAT_WS('#', emp_no, salary, from_date, to_date) AS row_text FROM salaries ORDER BY emp_no, from_date, to_date$$))); + +SELECT table_name, recs AS found_records, crc_md5 AS found_crc FROM found_values; + +SELECT + e.table_name, + CASE WHEN e.recs=f.recs THEN 'OK' ELSE 'not ok' END AS records_match, + CASE WHEN e.crc_md5=f.crc_md5 THEN 'ok' ELSE 'not ok' END AS crc_match +FROM + expected_values e INNER JOIN found_values f USING (table_name); + +SELECT 'CRC' AS summary, + CASE WHEN NOT EXISTS ( + SELECT 1 FROM expected_values e JOIN found_values f ON e.table_name=f.table_name WHERE f.crc_md5 != e.crc_md5 + ) THEN 'OK' ELSE 'FAIL' END AS result +UNION ALL +SELECT 'count', + CASE WHEN NOT EXISTS ( + SELECT 1 FROM expected_values e JOIN found_values f ON e.table_name=f.table_name WHERE f.recs != e.recs + ) THEN 'OK' ELSE 'FAIL' END; + +DROP FUNCTION compute_table_md5(TEXT); +DROP TABLE expected_values, found_values; diff --git a/postgresql/test_employees_sha.sql b/postgresql/test_employees_sha.sql new file mode 100644 index 00000000..0e7e7607 --- /dev/null +++ b/postgresql/test_employees_sha.sql @@ -0,0 +1,106 @@ +-- Test employees database integrity using SHA1 checksums +-- PostgreSQL version (requires pgcrypto extension) + +\connect employees + +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +SELECT 'TESTING INSTALLATION' AS info; + +DROP TABLE IF EXISTS expected_values, found_values; +CREATE TABLE expected_values ( + table_name VARCHAR(30) NOT NULL PRIMARY key, + recs INT NOT NULL, + crc_sha VARCHAR(100) NOT NULL, + crc_md5 VARCHAR(100) NOT NULL +); + + +CREATE TABLE found_values (LIKE expected_values); + +INSERT INTO expected_values VALUES +('employees', 300024,'4d4aa689914d8fd41db7e45c2168e7dcb9697359', + '4ec56ab5ba37218d187cf6ab09ce1aa1'), +('departments', 9,'4b315afa0e35ca6649df897b958345bcb3d2b764', + 'd1af5e170d2d1591d776d5638d71fc5f'), +('dept_manager', 24,'9687a7d6f93ca8847388a42a6d8d93982a841c6c', + '8720e2f0853ac9096b689c14664f847e'), +('dept_emp', 331603, 'd95ab9fe07df0865f592574b3b33b9c741d9fd1b', + 'ccf6fe516f990bdaa49713fc478701b7'), +('titles', 443308,'d12d5f746b88f07e69b9e36675b6067abb01b60e', + 'bfa016c472df68e70a03facafa1bc0a8'), +('salaries', 2844047,'b5a1785c27d75e33a4173aaa22ccf41ebd7d4a9f', + 'fd220654e95aea1b169624ffe3fca934'); +SELECT table_name, recs AS expected_records, crc_sha AS expected_crc FROM expected_values; + +-- Helper function: computes incremental SHA1 over rows returned by query. +-- The query must return a single column named "row_text". +-- Uses pgcrypto's digest() for SHA1 (MySQL's sha() equivalent). +CREATE OR REPLACE FUNCTION compute_table_sha1(p_query TEXT) +RETURNS TEXT +LANGUAGE plpgsql +AS $$ +DECLARE + crc TEXT := ''; + r RECORD; +BEGIN + FOR r IN EXECUTE p_query LOOP + crc := encode(digest(crc || '#' || r.row_text, 'sha1'), 'hex'); + END LOOP; + RETURN crc; +END; +$$; + +-- Compute checksums for each table +INSERT INTO found_values VALUES +('employees', + (SELECT COUNT(*) FROM employees), + (SELECT compute_table_sha1($$SELECT CONCAT_WS('#', emp_no, birth_date, first_name, last_name, gender, hire_date) AS row_text FROM employees ORDER BY emp_no$$)), + (SELECT compute_table_sha1($$SELECT CONCAT_WS('#', emp_no, birth_date, first_name, last_name, gender, hire_date) AS row_text FROM employees ORDER BY emp_no$$))), + +('departments', + (SELECT COUNT(*) FROM departments), + (SELECT compute_table_sha1($$SELECT CONCAT_WS('#', dept_no, dept_name) AS row_text FROM departments ORDER BY dept_no$$)), + (SELECT compute_table_sha1($$SELECT CONCAT_WS('#', dept_no, dept_name) AS row_text FROM departments ORDER BY dept_no$$))), + +('dept_manager', + (SELECT COUNT(*) FROM dept_manager), + (SELECT compute_table_sha1($$SELECT CONCAT_WS('#', dept_no, emp_no, from_date, to_date) AS row_text FROM dept_manager ORDER BY dept_no, emp_no$$)), + (SELECT compute_table_sha1($$SELECT CONCAT_WS('#', dept_no, emp_no, from_date, to_date) AS row_text FROM dept_manager ORDER BY dept_no, emp_no$$))), + +('dept_emp', + (SELECT COUNT(*) FROM dept_emp), + (SELECT compute_table_sha1($$SELECT CONCAT_WS('#', dept_no, emp_no, from_date, to_date) AS row_text FROM dept_emp ORDER BY dept_no, emp_no$$)), + (SELECT compute_table_sha1($$SELECT CONCAT_WS('#', dept_no, emp_no, from_date, to_date) AS row_text FROM dept_emp ORDER BY dept_no, emp_no$$))), + +('titles', + (SELECT COUNT(*) FROM titles), + (SELECT compute_table_sha1($$SELECT CONCAT_WS('#', emp_no, title, from_date, to_date) AS row_text FROM titles ORDER BY emp_no, title, from_date$$)), + (SELECT compute_table_sha1($$SELECT CONCAT_WS('#', emp_no, title, from_date, to_date) AS row_text FROM titles ORDER BY emp_no, title, from_date$$))), + +('salaries', + (SELECT COUNT(*) FROM salaries), + (SELECT compute_table_sha1($$SELECT CONCAT_WS('#', emp_no, salary, from_date, to_date) AS row_text FROM salaries ORDER BY emp_no, from_date, to_date$$)), + (SELECT compute_table_sha1($$SELECT CONCAT_WS('#', emp_no, salary, from_date, to_date) AS row_text FROM salaries ORDER BY emp_no, from_date, to_date$$))); + +SELECT table_name, recs AS found_records, crc_sha AS found_crc FROM found_values; + +SELECT + e.table_name, + CASE WHEN e.recs=f.recs THEN 'OK' ELSE 'not ok' END AS records_match, + CASE WHEN e.crc_sha=f.crc_sha THEN 'ok' ELSE 'not ok' END AS crc_match +FROM + expected_values e INNER JOIN found_values f USING (table_name); + +SELECT 'CRC' AS summary, + CASE WHEN NOT EXISTS ( + SELECT 1 FROM expected_values e JOIN found_values f ON e.table_name=f.table_name WHERE f.crc_sha != e.crc_sha + ) THEN 'OK' ELSE 'FAIL' END AS result +UNION ALL +SELECT 'count', + CASE WHEN NOT EXISTS ( + SELECT 1 FROM expected_values e JOIN found_values f ON e.table_name=f.table_name WHERE f.recs != e.recs + ) THEN 'OK' ELSE 'FAIL' END; + +DROP FUNCTION compute_table_sha1(TEXT); +DROP TABLE expected_values, found_values; diff --git a/postgresql/test_employees_sha2.sql b/postgresql/test_employees_sha2.sql new file mode 100644 index 00000000..4a8ea1b2 --- /dev/null +++ b/postgresql/test_employees_sha2.sql @@ -0,0 +1,94 @@ +-- Test employees database integrity using SHA-256 checksums +-- PostgreSQL version using pgcrypto extension + +\connect employees + +SELECT 'TESTING INSTALLATION' AS info; + +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +DROP TABLE IF EXISTS expected_values, found_values; +CREATE TABLE expected_values ( + table_name VARCHAR(30) NOT NULL PRIMARY KEY, + recs INT NOT NULL, + crc_sha2 VARCHAR(100) NOT NULL +); + +CREATE TABLE found_values (LIKE expected_values); + +-- Expected SHA-256 checksums (same values as MySQL's SHA2(..., 256)) +INSERT INTO expected_values VALUES +('employees', 300024, '21f5d003842f24853e251d3d5116798bafe257ec3d1bb448b5365b68deaabbf4'), +('departments', 9, '377c5d727383a32633e2973f8e3411beffe29e2f4cc297c586fa6b24aa7df9ba'), +('dept_manager', 24, '3a4e69723deec413a7d8a4f5ce55013830303fa617b6380ed2b0fd2d48b1c768'), +('dept_emp', 331603, '34548ee9989dd4d5e065168b43249c8d3eb48bfbbfb3f2fc1cf01be6658f6a75'), +('titles', 443308, 'a9e940ef9ba1029a8f0356fdbe495430bedc59eec5ceb4f71e0cc35ddcbf9980'), +('salaries', 2844047, '4e99e691a9ea98fefc0b4fec8ca4e758baeefba2967bd8d6474810a9a5f6e729'); +SELECT table_name, recs AS expected_records, crc_sha2 AS expected_crc FROM expected_values; + +-- Helper function: computes incremental SHA-256 over rows returned by query. +-- The query must return a single column named "row_text". +-- This replicates MySQL's @crc := SHA2(CONCAT_WS('#', @crc, ...), 256) +-- by using crc := encode(digest(crc || '#' || row_text, 'sha256'), 'hex') +CREATE OR REPLACE FUNCTION compute_table_sha256(p_query TEXT) +RETURNS TEXT +LANGUAGE plpgsql +AS $$ +DECLARE + crc TEXT := ''; + r RECORD; +BEGIN + FOR r IN EXECUTE p_query LOOP + crc := encode(digest(crc || '#' || r.row_text, 'sha256'), 'hex'); + END LOOP; + RETURN crc; +END; +$$; + +-- Compute checksums for each table +INSERT INTO found_values VALUES +('employees', + (SELECT COUNT(*) FROM employees), + (SELECT compute_table_sha256($$SELECT CONCAT_WS('#', emp_no, birth_date, first_name, last_name, gender, hire_date) AS row_text FROM employees ORDER BY emp_no$$))), + +('departments', + (SELECT COUNT(*) FROM departments), + (SELECT compute_table_sha256($$SELECT CONCAT_WS('#', dept_no, dept_name) AS row_text FROM departments ORDER BY dept_no$$))), + +('dept_manager', + (SELECT COUNT(*) FROM dept_manager), + (SELECT compute_table_sha256($$SELECT CONCAT_WS('#', dept_no, emp_no, from_date, to_date) AS row_text FROM dept_manager ORDER BY dept_no, emp_no$$))), + +('dept_emp', + (SELECT COUNT(*) FROM dept_emp), + (SELECT compute_table_sha256($$SELECT CONCAT_WS('#', dept_no, emp_no, from_date, to_date) AS row_text FROM dept_emp ORDER BY dept_no, emp_no$$))), + +('titles', + (SELECT COUNT(*) FROM titles), + (SELECT compute_table_sha256($$SELECT CONCAT_WS('#', emp_no, title, from_date, to_date) AS row_text FROM titles ORDER BY emp_no, title, from_date$$))), + +('salaries', + (SELECT COUNT(*) FROM salaries), + (SELECT compute_table_sha256($$SELECT CONCAT_WS('#', emp_no, salary, from_date, to_date) AS row_text FROM salaries ORDER BY emp_no, from_date, to_date$$))); + +SELECT table_name, recs AS found_records, crc_sha2 AS found_crc FROM found_values; + +SELECT + e.table_name, + CASE WHEN e.recs=f.recs THEN 'OK' ELSE 'not ok' END AS records_match, + CASE WHEN e.crc_sha2=f.crc_sha2 THEN 'ok' ELSE 'not ok' END AS crc_match +FROM + expected_values e INNER JOIN found_values f USING (table_name); + +SELECT 'CRC' AS summary, + CASE WHEN NOT EXISTS ( + SELECT 1 FROM expected_values e JOIN found_values f ON e.table_name=f.table_name WHERE f.crc_sha2 != e.crc_sha2 + ) THEN 'OK' ELSE 'FAIL' END AS result +UNION ALL +SELECT 'count', + CASE WHEN NOT EXISTS ( + SELECT 1 FROM expected_values e JOIN found_values f ON e.table_name=f.table_name WHERE f.recs != e.recs + ) THEN 'OK' ELSE 'FAIL' END; + +DROP FUNCTION compute_table_sha256(TEXT); +DROP TABLE expected_values, found_values; diff --git a/test_employees_sha2.sql b/test_employees_sha2.sql new file mode 100644 index 00000000..7d23e1b6 --- /dev/null +++ b/test_employees_sha2.sql @@ -0,0 +1,87 @@ +-- Test employees database integrity using SHA-256 checksums +-- Uses SHA2() which is available on all MySQL versions (8.0+) +-- This test works on MySQL 9.6+ where md5() and sha() have been removed + +USE employees; + +SELECT 'TESTING INSTALLATION' as 'INFO'; + +DROP TABLE IF EXISTS expected_values, found_values; +CREATE TABLE expected_values ( + table_name varchar(30) not null primary key, + recs int not null, + crc_sha2 varchar(100) not null +); + + +CREATE TABLE found_values (LIKE expected_values); + +-- Expected SHA-256 checksums (computed from the canonical data set) +INSERT INTO `expected_values` VALUES +('employees', 300024, '21f5d003842f24853e251d3d5116798bafe257ec3d1bb448b5365b68deaabbf4'), +('departments', 9, '377c5d727383a32633e2973f8e3411beffe29e2f4cc297c586fa6b24aa7df9ba'), +('dept_manager', 24, '3a4e69723deec413a7d8a4f5ce55013830303fa617b6380ed2b0fd2d48b1c768'), +('dept_emp', 331603, '34548ee9989dd4d5e065168b43249c8d3eb48bfbbfb3f2fc1cf01be6658f6a75'), +('titles', 443308, 'a9e940ef9ba1029a8f0356fdbe495430bedc59eec5ceb4f71e0cc35ddcbf9980'), +('salaries', 2844047, '4e99e691a9ea98fefc0b4fec8ca4e758baeefba2967bd8d6474810a9a5f6e729'); +SELECT table_name, recs AS expected_records, crc_sha2 AS expected_crc FROM expected_values; + +DROP TABLE IF EXISTS tchecksum; +CREATE TABLE tchecksum (chk char(100)); + +SET @crc= ''; +INSERT INTO tchecksum + SELECT @crc := SHA2(CONCAT_WS('#',@crc, + emp_no,birth_date,first_name,last_name,gender,hire_date), 256) + FROM employees ORDER BY emp_no; +INSERT INTO found_values VALUES ('employees', (SELECT COUNT(*) FROM employees), @crc); + +SET @crc = ''; +INSERT INTO tchecksum + SELECT @crc := SHA2(CONCAT_WS('#',@crc, dept_no,dept_name), 256) + FROM departments ORDER BY dept_no; +INSERT INTO found_values VALUES ('departments', (SELECT COUNT(*) FROM departments), @crc); + +SET @crc = ''; +INSERT INTO tchecksum + SELECT @crc := SHA2(CONCAT_WS('#',@crc, dept_no,emp_no, from_date,to_date), 256) + FROM dept_manager ORDER BY dept_no,emp_no; +INSERT INTO found_values VALUES ('dept_manager', (SELECT COUNT(*) FROM dept_manager), @crc); + +SET @crc = ''; +INSERT INTO tchecksum + SELECT @crc := SHA2(CONCAT_WS('#',@crc, dept_no,emp_no, from_date,to_date), 256) + FROM dept_emp ORDER BY dept_no,emp_no; +INSERT INTO found_values VALUES ('dept_emp', (SELECT COUNT(*) FROM dept_emp), @crc); + +SET @crc = ''; +INSERT INTO tchecksum + SELECT @crc := SHA2(CONCAT_WS('#',@crc, emp_no, title, from_date,to_date), 256) + FROM titles ORDER BY emp_no,title, from_date; +INSERT INTO found_values VALUES ('titles', (SELECT COUNT(*) FROM titles), @crc); + +SET @crc = ''; +INSERT INTO tchecksum + SELECT @crc := SHA2(CONCAT_WS('#',@crc, emp_no, salary, from_date,to_date), 256) + FROM salaries ORDER BY emp_no,from_date,to_date; +INSERT INTO found_values VALUES ('salaries', (SELECT COUNT(*) FROM salaries), @crc); + +DROP TABLE tchecksum; + +SELECT table_name, recs AS found_records, crc_sha2 AS found_crc FROM found_values; + +SELECT + e.table_name, + IF(e.recs=f.recs,'OK', 'not ok') AS records_match, + IF(e.crc_sha2=f.crc_sha2,'ok','not ok') AS crc_match +FROM + expected_values e INNER JOIN found_values f USING (table_name); + +SET @crc_fail=(SELECT COUNT(*) FROM expected_values e INNER JOIN found_values f ON (e.table_name=f.table_name) WHERE f.crc_sha2 != e.crc_sha2); +SET @count_fail=(SELECT COUNT(*) FROM expected_values e INNER JOIN found_values f ON (e.table_name=f.table_name) WHERE f.recs != e.recs); + +SELECT 'CRC' AS summary, IF(@crc_fail = 0, "OK", "FAIL") AS `result` +UNION ALL +SELECT 'count', IF(@count_fail = 0, "OK", "FAIL") AS `count`; + +DROP TABLE expected_values, found_values;