4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Happy Birthday to PostgreSQL!

4
Last updated at Posted at 2025-12-23

この記事はPostgreSQL Advent Calendar 2025の24日目の記事です。

はじめに

2025年も残すところあと1週間。どんな1年でしたでしょうか?
個人的には、好きなものの40周年記念イベントが多い年でした。ビックリマンとかドラゴンボールとかマリオとか。
ビックリマンは復刻版が販売されたのでいくつか購入し、久しぶりに「あのドキドキ感」を堪能しました!(見事、ヘッドロココGet!)
でだ。
ふと、「PostgreSQLは何周年なのかしら?」とマニュアル調べたら、来年が開発開始から40周年になるっぽい。
2. A Brief History of PostgreSQL

The implementation of POSTGRES began in 1986.

ということで、今年はPostgreSQLの40周年のお祝いをいたします!(ちょっとフラゲですが)

何する?

せっかくなので他の40周年のものも含めようと考えました。タイミングよく、みんな大好き『ゼルダの伝説』が発売40周年を迎えるので、コラボします。

Wikipedia:ゼルダの伝説

日本では1986年2月21日にファミリーコンピュータ ディスクシステム用として、日本国外のディスクシステムが発売されなかった地域ではロムカセットとして発売された。

大好きなPostgreSQLと大好きなゼルダをコラボできることに震えが止まりません。

以降は完全に自己満足の世界です。
暖かい目で見てくださいorz

で、どうする?

色々コラボ案はありそうですが、今回は初代・ゼルダの伝説のハイラルマップをPostgreSQLで再現しましょう。

制約と誓約

単に実装するだけだと愛が伝わらなそうなので、次の制約を課しました。

  1. Webブラウザとかに頼らない。クライアント・アプリケーションはターミナルOnlyで!
  2. bytea列にPNG画像を格納して表示するなんて軟弱なことはしない。ダメぜったい
  3. マップなんだから、道があれば上下左右移動できるようにしよう

下ごしらえ

制約を遵守するために諸々の工夫を行います。

ターミナルだけで画像表示する準備

調べてみると、VSCode使って設定有効化するとVSCodeのTerminalで画像表示できるらしい。
VSCode のターミナル内で画像を表示できるようになったので試してみた
ターミナルで画像を表示する Sixel Graphics について

VSCodeの設定変更

設定あった。ポチッとな。
VSCodeSettings.png

img2sixelで表示

libsixelを入れればよいようなのでサクッと。

$ brew install libsixel
$ which img2sixel
/usr/local/bin/img2sixel

画像ファイルも表示できたのでOK!
img2sixel.png

bytea型以外で画像を扱う準備

画像フォーマットがバイナリだとNG(制約違反)なので、PPM形式:ASCIIでRGB値を取り扱うことにします。ドット絵なので相性もよさそうだし。

画像作成

とはいえ、いきなりPPM形式の画像を1から作成するのは骨が折れるので、「ドット絵->PNG画像->PPM画像」と変形していくことにします。
ドット絵の作成は、ミニドット絵メーカー3というサイトを利用させていただきました。感謝。
PNG画像として出力したものを、一度GIMPで読み込んで、PPM形式で出力(Export As...でできる)したら画像作成完了です。
ミニドット絵メーカー3.png

img2sixelでPPM形式の画像も表示できたので、イケそうです。
img2sixel_ppm.png

データ格納

RGB値を保持する pixel 型を作成し、32x32の配列にPPMから抜き出した各ピクセルのRGB値を格納します。

CREATE TYPE pixel AS (r INT, g INT, b INT);
CREATE TABLE objects (
        object_id INT,
        p pixel[32][32]
);

これを、マップ上にあるオブジェクト(山とか海とか岩とか)の分だけ用意します。その数90種類!

マップを行き来できるようにする準備

ハイラルの世界は128(横16x縦8)個のステージでできてて、各ステージは176(横16x縦11)個のオブジェクトでできてます。

マップデータ作成

マップ=128ステージなので、stages テーブルを用意し、それぞれ176個のオブジェクトを関連づけます。

CREATE TABLE stages (
        stage_id INT,
        up_arrow INT,
        down_arrow INT,
        right_arrow INT,
        left_arrow INT,
        object_ids INT[11][16]
);

図示すると、こんな感じ。
figure1.png

行き来する処理

描画は下記のスクリプトで行います。
キーボードの押された値を取得し、h,j,k,lであればそれぞれ左、上、下、右に移動します。
ただし、up_arrow, down_arrow, right_arrow, left_arrowの値を見て、その値が 0 (道がない) 場合は移動しません。

#!/bin/bash

DBNAME=$1;
STAGE_ID=120;

while : ; do
  printf "%s\n%d %d\n%d\n" P3 512 352 255 > rebuild.ppm;
  ./render ${DBNAME} ${STAGE_ID} | sed 's/[{|}|"|(|)]//g' | sed 's/,/ /g' | sed 's/ /\n/g' >> rebuild.ppm;
  clear;
  img2sixel rebuild.ppm 2> /dev/null; echo "";

  while IFS= read -r -n1 -s key; do
    case $key in
      "j")
        next_stage=`psql -At ${DBNAME} -c "SELECT up_arrow FROM stages WHERE stage_id = $STAGE_ID"`;;
      "k")
        next_stage=`psql -At ${DBNAME} -c "SELECT down_arrow FROM stages WHERE stage_id = $STAGE_ID"`;;
      "l")
        next_stage=`psql -At ${DBNAME} -c "SELECT right_arrow FROM stages WHERE stage_id = $STAGE_ID"`;;
      "h")
        next_stage=`psql -At ${DBNAME} -c "SELECT left_arrow FROM stages WHERE stage_id = $STAGE_ID"`;;
      "q") exit;;
      *) continue;;
    esac

    if [ $next_stage -ne 0 ]; then
      STAGE_ID=$next_stage;
      break;
    fi
  done
done;

配列型の扱い

上記を見てわかるように、今回配列型を多用しました。
最初、スクリプトの中で扱おうと思ったのですが、2次元配列の扱いが難しく、愚直に処理したら描画がものすごい時間かかる結果となりました...(1ステージ表示するのに30秒くらいかかる)
ダメだこりゃ\(^o^)/
ということで、急遽C言語で書き直しました。(前述のスクリプト中の render ってやつ)
1から作成するのは骨が折れるので、生成AIにベースを作ってもらい、それをサクッとカスタマイズして作りました。(これで1ステージ表示するの1秒程度になったのでよしとする)

生成AIに助けてもらった内容

「質問」

libpqでPostgreSQLの2次元配列型のデータを処理するC言語プログラムを作成してください。

「回答」

PostgreSQL の C クライアントライブラリである libpq を使って、PostgreSQL の 2 次元配列型のデータ(例:integer[][])を取得・解析する C プログラムのサンプルを以下に示します。

このサンプルでは、配列型を文字列として受け取り、C のコードで 2 次元配列として解析します。

---

## 🔧 前提:テーブル定義(PostgreSQL)

```sql
CREATE TABLE my_table (
    id serial PRIMARY KEY,
    matrix integer[][]
);

INSERT INTO my_table (matrix) VALUES
('{{1,2,3},{4,5,6}}'),
('{{7,8},{9,10}}');

🧠 libpq C プログラム:2次元配列の解析

#include <stdio.h>
#include <stdlib.h>
#include <libpq-fe.h>
#include <string.h>

// 文字列から空白をスキップ
void skip_whitespace(const char **p) {
    while (**p == ' ' || **p == '\n' || **p == '\t') (*p)++;
}

// PostgreSQL の配列表現(例: "{{1,2},{3,4}}")を C の int 配列にパース
int **parse_pg_array_2d(const char *input, int *out_rows, int *out_cols) {
    const char *p = input;
    skip_whitespace(&p);

    if (*p != '{') return NULL;
    p++; // skip first '{'

    int rows_alloc = 10;
    int **matrix = malloc(rows_alloc * sizeof(int *));
    int row_count = 0;
    int col_count = -1;

    while (*p && *p != '}') {
        skip_whitespace(&p);
        if (*p != '{') break;

        p++; // skip '{'
        int cols_alloc = 10;
        int *row = malloc(cols_alloc * sizeof(int));
        int col = 0;

        while (*p && *p != '}') {
            skip_whitespace(&p);
            int val = atoi(p);
            row[col++] = val;

            // skip number
            while (*p && (*p == '-' || (*p >= '0' && *p <= '9'))) p++;

            skip_whitespace(&p);
            if (*p == ',') p++;
        }

        if (*p == '}') p++;

        // 初回で列数を記録
        if (col_count == -1) col_count = col;

        if (row_count >= rows_alloc) {
            rows_alloc *= 2;
            matrix = realloc(matrix, rows_alloc * sizeof(int *));
        }

        matrix[row_count++] = row;

        skip_whitespace(&p);
        if (*p == ',') p++;
    }

    if (*p == '}') p++;  // 最後の '}' をスキップ

    *out_rows = row_count;
    *out_cols = col_count;
    return matrix;
}

int main() {
    const char *conninfo = "dbname=your_database";
    PGconn *conn = PQconnectdb(conninfo);

    if (PQstatus(conn) != CONNECTION_OK) {
        fprintf(stderr, "Connection error: %s\n", PQerrorMessage(conn));
        PQfinish(conn);
        return 1;
    }

    PGresult *res = PQexec(conn, "SELECT matrix FROM my_table");

    if (PQresultStatus(res) != PGRES_TUPLES_OK) {
        fprintf(stderr, "Query error: %s\n", PQerrorMessage(conn));
        PQclear(res);
        PQfinish(conn);
        return 1;
    }

    int rows = PQntuples(res);
    for (int i = 0; i < rows; i++) {
        char *matrix_str = PQgetvalue(res, i, 0);
        printf("Row %d matrix: %s\n", i, matrix_str);

        int nrows, ncols;
        int **matrix = parse_pg_array_2d(matrix_str, &nrows, &ncols);

        if (!matrix) {
            fprintf(stderr, "Failed to parse matrix.\n");
            continue;
        }

        for (int r = 0; r < nrows; r++) {
            for (int c = 0; c < ncols; c++) {
                printf("  [%d][%d] = %d\n", r, c, matrix[r][c]);
            }
            free(matrix[r]);
        }
        free(matrix);
    }

    PQclear(res);
    PQfinish(conn);
    return 0;
}

🔧 コンパイル方法

libpq をリンクする必要があります:

gcc -o pg_array_parser pg_array_parser.c -lpq

✅ 出力例

Row 0 matrix: {{1,2,3},{4,5,6}}
  [0][0] = 1
  [0][1] = 2
  [0][2] = 3
  [1][0] = 4
  [1][1] = 5
  [1][2] = 6

📌 補足

  • このコードは、PostgreSQL の 2 次元配列を文字列として受け取り、C 言語でネストされた波括弧とカンマを手動解析します。
  • 型は int 固定。floatやtext型にしたい場合は変更が必要です。
  • NULL 値はサポートしていません。

必要に応じて、可変長行列やNULL対応、他の型への対応もご案内できます。ご希望があればお知らせください。

ハイラルの歩き方

諸々の準備ができたので、ハイラルを徘徊してみます!

プレイ画面

アニメーション

こんな感じで動きます。
playing_zelda_resize.gif

鳥瞰図

ハイラル全体はこんな感じ。
HyruleMap.png

おすすめスポット

はじまりの台地

スタート地点です。(stage_id=120)
120.png

Waterfall

水飛沫が飛んできそうです。(stage_id=27)
027.png

いざ、決戦!

ラスボス・ガノンがいるダンジョンです。(stage_id=6)
6.png

現場からは以上です。

まとめ

今年は、同級生コンビのPostgreSQL&ゼルダの伝説で奇跡のコラボ作品を作成しました。
学びとしては、

  1. sixelプロトコルを用いて、ターミナルで画像表示を行えた
  2. PPMのRGB値をPostgreSQLに格納し、そのデータを元にPPM画像を復元できた
  3. 2次元配列を処理するプログラムを生成AIを元に作成し、処理性能を改善できた

これからも楽しくPostgreSQLライフを送りたいと思います!
Merry Christmas & Happy New Year!

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?