Oracle 動的SQL 方法4を動くようにしてみた(Pro*C)

2013/11/29 2020/05/22

仕事でPro*C上から動的SQL 方法4(選択リストもプレースホルダ(バインド変数)も数が不定)を使わないといけなくなりそうになったので、検証の為Pro*C/C++ プログラマーズ・ガイドを見つつ、そこにあるサンプルを実機でまずは動かしてみようと試してみました。

そしたら動かしてるマシンにも問題ありそうですが、procを流した時点でエラーがワラワラと…
なんとかあちこち直したら動くようになったので、参考までに公開します。

#あくまでも個人的に直した物なので何か起きたとしてもOracle社はもちろん、私も責任取りませんのであしからず。
 (ただ直さないと何か起きました(–; )

修正点は、

  • SQL_SINGLE_RCTXが構文エラー(ヘッダに書かれてるdvoid型を認識してくれてない)になるので強引にソース上に再定義
  • SQLColumnNullCheckのパラメータが1つ足りないので再定義(このサンプル本当に動いてたのか?)
  • 変数の初期化が怪しい所がいくつかあったので修正。(バッファオーバーフロー起こしてるような所があった)
  • DB接続時にtns名も指定出来るように改造

です。
コメントも書き直しました。(元を翻訳した訳ではありません)
最初すご~く長いソースだったのでSQL実行して結果取るだけでこんなに書かなきゃいけないの?と思ったら、入力プロンプトの制御とかが入っててそうでも無い事にじっくり読んでから気付きました。諦めないで良かった。

一応ファイルでも置いておきます。(ShuftJISです) sono4.pc
 

/*******************************************************************
サンプルプログラム10:  動的SQLその4 
取得項目もWhere句も不定なSQLを処理する際のサンプル

どうも元のサンプルが古くコンパイルがそのまま通らなかった。
以下を中心に直した。
・SQL_SINGLE_RCTXがsqlcpr.hで(dvoid *)0で定義されていたが構文エラー
 になる。
 多分dvoid型がどこにも宣言されてないからだと思われる。
 とりあえず本ソース上で0で再定義した。
・SQLColumnNullCheckのパラメータが1つ足りない。
 ググった感じでは第一パラメータにSQL_SINGLE_RCTXを指定するようだっ
 たので指定してみた。
・DB接続時にtns指定をするようにした。
・変数の初期化が怪しい箇所の修正
*******************************************************************/
#include <stdio.h>
#include <string.h>
#include <setjmp.h>
#include <sqlda.h>
#include <stdlib.h>
#include <sqlcpr.h>

/* 元はsqlcpr.hで(dvoid *)0で定義されていたが構文エラーとなるので
   強制的に0で再定義                                             */
#define SQL_SINGLE_RCTX 0

#define MAX_ITEMS         40    /* Selectで指定できる最大項目数 */

/* Maximum lengths of the _names_ of the
   select-list items or indicator variables. */
#define MAX_VNAME_LEN     100   /* 項目名最大長 */
#define MAX_INAME_LEN     30

#ifndef NULL
#define NULL  0
#endif

/* Prototypes */
#if defined(__STDC__)
  void sql_error(void);
  int oracle_connect(void);
  int alloc_descriptors(int, int, int);
  int get_dyn_statement(void);
  void set_bind_variables(void);
  void process_select_list(void);
  void help(void);
#else
  void sql_error(/*_ void _*/);
  int oracle_connect(/*_ void _*/);
  int alloc_descriptors(/*_ int, int, int _*/);
  int get_dyn_statement(/* void _*/);
  void set_bind_variables(/*_ void -*/);
  void process_select_list(/*_ void _*/);
  void help(/*_ void _*/);
#endif

char *dml_commands[] = {"SELECT", "select", "INSERT", "insert",
                        "UPDATE", "update", "DELETE", "delete"};

EXEC SQL INCLUDE sqlda;
EXEC SQL INCLUDE sqlca;

EXEC SQL BEGIN DECLARE SECTION;
    char    dyn_statement[1024];
    EXEC SQL VAR dyn_statement IS STRING(1024);
EXEC SQL END DECLARE SECTION;
 
SQLDA *bind_dp;
SQLDA *select_dp;

jmp_buf jmp_continue;   /* setjmpマクロ用。処理位置保持変数 */

/* A global flag for the error routine. */
int parse_flag = 0;

void main()
{
    int i;

    if (oracle_connect() != 0) exit(1); /* DB接続開始 */

    /* 選択リストとプレースホルダの記憶領域の確保と初期化 */
    if (alloc_descriptors(MAX_ITEMS, MAX_VNAME_LEN, MAX_INAME_LEN) != 0) exit(1);

    /* Process SQL statements. */
    for (;;) 
    {
        /* この位置を保持しlongjmpされたらここに戻ってくる */
        (void) setjmp(jmp_continue);

        /* SQL入力を受付、dyn_statementに保持(EXIT=break) */
        if (get_dyn_statement() != 0) break;

        /* SQLエラー発生時はエラー処理へ飛ぶ */
        EXEC SQL WHENEVER SQLERROR DO sql_error();

        parse_flag = 1;     /* sql_error()でのパースエラー判別用 */
        EXEC SQL PREPARE S FROM :dyn_statement;
        parse_flag = 0;     /* パース通過したらフラグ変更 */

        /* カーソル定義 */
        EXEC SQL DECLARE C CURSOR FOR S;

        /* プレースホルダ(バインド変数)への値セット */
        set_bind_variables();

        /* カーソルオープン */
        EXEC SQL OPEN C USING DESCRIPTOR bind_dp;

        /* SQL結果表示 */
        process_select_list();

        /* dml_commands数分ループ */
        for (i = 0; i < 8; i++)
        {   /* SQLの先頭を検証し、PL/SQLブロックで無ければメッセージを表示 */
           if (strncmp(dyn_statement, dml_commands[i], 6) == 0)
           {
               printf("\n\n%d行処理しました。\n", sqlca.sqlerrd[2]);
               break;
           }
        }
    }   /* 1回のステートメントの終端。EXIT入力まで無限ループ */

    /* 確保したメモリを解放する */
    for (i = 0; i < MAX_ITEMS; i++)
    {   /* 元は予想長さが0バイト以外なら解放だったが元々1バイトは確保しているので修正 */
            free(bind_dp->V[i]);
            free(bind_dp->I[i]);
            free(select_dp->V[i]);
            free(select_dp->I[i]);
    }

    /* 選択リストとプレースホルダ用に確保した記憶域を解放する */
    SQLSQLDAFree( SQL_SINGLE_RCTX, bind_dp);
    SQLSQLDAFree( SQL_SINGLE_RCTX, select_dp);

    EXEC SQL WHENEVER SQLERROR CONTINUE;    /* 終わるだけなのでエラー発生しても無視 */
    EXEC SQL CLOSE C;                       /* カーソルクローズ */
    EXEC SQL COMMIT WORK RELEASE;           /* トランザクションをコミットし、DB切断 */
    puts("\n終了します。\n");

    EXEC SQL WHENEVER SQLERROR DO sql_error();  /* 以降のエラーはsql_errorで処理 */
}

/* DB接続サブ */
int oracle_connect()
{
    EXEC SQL BEGIN DECLARE SECTION;
        VARCHAR  username[128];         /* ユーザー名 */
        VARCHAR  password[32];          /* パスワード */
        VARCHAR  tns[32];               /* TNS名 */
    EXEC SQL END DECLARE SECTION;

    printf("\nusername: "); /* ユーザー名の入力を受付 */
    fgets((char *) username.arr, sizeof username.arr, stdin);
    username.arr[strlen((char *) username.arr)-1] = '\0';
    username.len = (unsigned short)strlen((char *) username.arr);

    printf("password: ");   /* パスワードの入力を受付 */
    fgets((char *) password.arr, sizeof password.arr, stdin);
    password.arr[strlen((char *) password.arr) - 1] = '\0';
    password.len = (unsigned short)strlen((char *) password.arr);

    printf("TNS: ");        /* TNS名の入力を受付 */
    fgets((char *) tns.arr, sizeof tns.arr, stdin);
    tns.arr[strlen((char *) tns.arr) - 1] = '\0';
    tns.len = (unsigned short)strlen((char *) tns.arr);

    EXEC SQL WHENEVER SQLERROR GOTO connect_error;
    EXEC SQL CONNECT :username IDENTIFIED BY :password USING :tns;
    printf("\nConnected to ORACLE as user %s.\n", username.arr);
    return 0;

connect_error:  /* 接続失敗時 */
    fprintf(stderr, "Cannot connect to ORACLE as user %s\n", username.arr);
    return -1;
}


/* 選択リストとプレースホルダの記憶領域の確保と初期化 */
int alloc_descriptors(int size, int max_vname_len, int max_iname_len)
{
    int i;

    /* SQLSQLDAAlloc : 記憶域の割り当て */
    /* 1:ランタイム・コンテキストへのポインタ */
    /* 2:選択リストの項目またはプレーすホルダ(=バインド変数)の最大数 */
    /* 3:選択リスト名またはプレースホルダ名の最大長 */
    if ((bind_dp =
       SQLSQLDAAlloc(SQL_SINGLE_RCTX, size, max_vname_len, max_iname_len)) == 
         (SQLDA *) 0)
    {   /* 割り当てに失敗した場合 */
        fprintf(stderr, "バインド変数用メモリの確保に失敗しました");
        return -1;  /* Have to exit in this case. */
    }

    if ((select_dp =
        SQLSQLDAAlloc (SQL_SINGLE_RCTX, size, max_vname_len, max_iname_len)) == 
           (SQLDA *) 0)
    {
        fprintf(stderr, "選択リスト用メモリの確保に失敗しました");
        return -1;
    }

    /* 最大項目数分、メモリを確保する */
    /* I変数…標識変数を確保するデータバッファのアドレス配列へのポインタ */
    /*        選択リスト時はi番目の値がNULLの場合は-1が入る。            */
    /*                      以外の場合は0が整数が入る                    */
    /*        プレースホルダ時は上記に沿いバインド変数のNULL状態を表す   */
    /* V変数…実際の値を可能するデータバッファのアドレス配列へのポインタ */
    for (i = 0; i < MAX_ITEMS; i++) {
        bind_dp->I[i] = (short *) malloc(sizeof (short));
        select_dp->I[i] = (short *) malloc(sizeof(short));
        bind_dp->V[i] = (char *) malloc(1);     /* 仮に1バイトで確保 */
        select_dp->V[i] = (char *) malloc(1);   /* 仮に1バイトで確保 */
    }
       
    return 0;
}

/* SQL入力プロンプトサブ */
/* dyn_statementに入力されたSQL(改行排除)が入る */
int get_dyn_statement()
{
    char *cp, linebuf[256];
    int iter, plsql;

    for (plsql = 0, iter = 1; ;)
    {
        if (iter == 1)
        {
            printf("\nSQL> ");
            dyn_statement[0] = '\0';    /* 入力蓄積内容をクリア */
        }
        
        fgets(linebuf, sizeof linebuf, stdin);  /* 1行入力を受け付ける */
        cp = strrchr(linebuf, '\n');            /* 後ろから\nを探す */
        if (cp && cp != linebuf)                /* 先頭以外なら空白に置換 */
            *cp = ' ';

        if ((strncmp(linebuf, "EXIT", 4) == 0) || (strncmp(linebuf, "exit", 4) == 0))
        {   /* EXITが入力された場合 */
            return -1;
        }
        else if (linebuf[0] == '?' ||
            (strncmp(linebuf, "HELP", 4) == 0) ||
            (strncmp(linebuf, "help", 4) == 0))
        {   /* HELP指示が出た場合はメッセージを出力 */
            help();
            iter = 1;   /* 行番号を1に設定し直す */
            continue;
        }

        if (strstr(linebuf, "BEGIN") ||
            (strstr(linebuf, "begin")))
        {   /* BEGINが含まれていた場合はPL/SQLブロックと判断する */
            plsql = 1;
        }

        /* 未入力でなければ入力内容を蓄積する */
        if (cp != linebuf) strcat(dyn_statement, linebuf);

        if ((plsql && (cp = strrchr(dyn_statement, '/'))) ||
            (!plsql && (cp = strrchr(dyn_statement, ';'))))
        {   /* 最後の文字が;か/ならそこを最後にしてループを抜ける */
            *cp = '\0';
            break;
        }

        iter++;                 /* 行番号を加算して表示 */
        printf("%3d  ", iter);
    }
    return 0;
}

/* プレースホルダ(バインド変数)への値セット */
void set_bind_variables()
{
    int i, n;
    char bind_var[64];

    EXEC SQL WHENEVER SQLERROR DO sql_error();  /* エラーハンドリング指定 */
    bind_dp->N = MAX_ITEMS; /* プレースホルダ指定可能最大項目数を指定(DESCRIBEの前でないとダメそう) */
    EXEC SQL DESCRIBE BIND VARIABLES FOR S INTO bind_dp;    /* バインド変数割り当て */

    if (bind_dp->F < 0)
    {   /* SQL解析しプレースホルダの数が最大値を超えた場合はマイナスとなる */
        printf ("\nバインド変数を最大数を超えて指定しています。個数(%d) 最大数(%d)\n.", -bind_dp->F, MAX_ITEMS);
        return;
    }

    bind_dp->N = bind_dp->F;    /* 取得したバインド変数の数で最大変数数を上書きする */

    /* バインド変数の数分ループ */
    for (i = 0; i < bind_dp->F; i++)
    {
        /* (int)bind_dp->C[i]でバインド変数名の長さが分かる */
        printf ("\nバインド変数の値を入力して下さい。%s:  ",bind_dp->S[i]);
        fgets(bind_var, sizeof bind_var, stdin);

        n = strlen(bind_var) - 1;   /* 改行を除いた長さをL変数にセット */
        bind_dp->L[i] = n;

        /* V変数に入力内容をセットする(+1は\0の分) */
        bind_dp->V[i] = (char *) realloc(bind_dp->V[i], n+1);
        memset( bind_dp->V[i], 0x00, n+1 );
        strncpy(bind_dp->V[i], bind_var, n);    /* 改行を除いてセット */

        /* NULLの場合はI変数に-1(NULL)を設定。NULL以外は0(値有り)を設定 */
        if ((strncmp(bind_dp->V[i], "NULL", 4) == 0) ||
                (strncmp(bind_dp->V[i], "null", 4) == 0))
            *bind_dp->I[i] = -1;
        else
            *bind_dp->I[i] = 0;
    
        bind_dp->T[i] = 1;  /* T変数(データ型)に1(VARCHAR2)をセット */
    }
}


/* 選択リスト情報をSQLDA変数にセットし、SQL結果を取得/表示する */
void process_select_list()
{
    int i, null_ok, precision, scale;
    char title[MAX_VNAME_LEN];  /* 選択リスト名 */ 

    if ((strncmp(dyn_statement, "SELECT", 6) != 0) &&
        (strncmp(dyn_statement, "select", 6) != 0))
    {   /* SELECT以外の場合はF変数(選択リスト数)に0をセットして戻る */
        select_dp->F = 0;
        return;
    }

    select_dp->N = MAX_ITEMS;   /* 選択リスト指定可能最大項目数を指定(DESCRIBEの前でないとダメそう) */
    EXEC SQL DESCRIBE SELECT LIST FOR S INTO select_dp; /* 選択リスト割り当て */

    if (select_dp->F < 0)
    {   /* SQL解析し選択リスト数が最大値を超えた場合はマイナスとなる */
        printf ("\n選択リスト数が最大数を超えて指定されています。個数(%d) 最大数(%d)\n", -(select_dp->F), MAX_ITEMS);
        return;
    }

    select_dp->N = select_dp->F;    /* 取得した選択リスト数で最大変数数を上書きする */

    printf ("\n");
    /* 選択リスト数分ループ */
    for (i = 0; i < select_dp->F; i++)
    {
        /* T変数に付与されたデータ型のNULL/NOT NULLビットを消去する */
        SQLColumnNullCheck (SQL_SINGLE_RCTX,(unsigned short *)&(select_dp->T[i]), 
             (unsigned short *)&(select_dp->T[i]), &null_ok);
        /* データ型で分岐し、L変数(各選択リストの長さ)を設定する */
        switch (select_dp->T[i])
        {
            case  1 :   /* VARCHAR2の場合(特に変換しない) */
                break;
            case  2 :   /* NUMBERの場合(SQLNumberPrecV6()で変換する */
                /* SQLNumberPrecV6 : 精度と位取りの抽出 */
                /* 1:ランタイム・コンテキストへのポインタ */
                /* 2:NUMBER値を保存する長い整変数へのポインタ(位取り:上位バイト,精度;下位バイト) */
                /* 3:NUMBER値の精度(有効桁数)を戻す整変数へのポインタ */
                /* 4:NUMBER値の位取り(四捨五入する位置)を戻す整変数へのポインタ */
                SQLNumberPrecV6( SQL_SINGLE_RCTX, (unsigned long *)&(select_dp->L[i]), &precision, &scale);
                /* 精度が0(サイズ指定の無いNUMBER)の場合は最大精度とする */
                if (precision == 0) precision = 40;
                /* 位取りがプラス(小数)かどうかでL変数のサイズを決定する */
                if (scale > 0)
                    select_dp->L[i] = sizeof(float);    /* float型に変換 */
                else
                    select_dp->L[i] = sizeof(int);      /* int型に変換 */
                break;
            case  8 :   /* LONGの場合 */
                select_dp->L[i] = 240;
                break;
            case 11 :   /* ROWIDの場合 */
                select_dp->L[i] = 18;
                break;
            case 12 :   /* DATEの場合 */
                /* T変数を再設定しない場合は7バイト内部形式となる */
                select_dp->L[i] = 9;
                break;
            case 23 :   /* RAWの場合 */
                break;
            case 24 :   /* LONG RAWの場合 */
                select_dp->L[i] = 240;
                break;
        }
        /* データ型がNUMBER以外の場合はL変数の長さ+1(\0)分の領域を確保する */
        if (select_dp->T[i] != 2)
            select_dp->V[i] = (char *) realloc(select_dp->V[i], select_dp->L[i] + 1);  
        else    /* NUMBERの場合はL変数の長さ分の領域を確保する */
            select_dp->V[i] = (char *) realloc(select_dp->V[i], select_dp->L[i]);  
        /* 選択リスト名を表示する */
        memset(title, ' ', MAX_VNAME_LEN-1);
        title[MAX_VNAME_LEN-1] = '\0';
        strncpy(title, select_dp->S[i], select_dp->C[i]);   /* スペース埋め */
        if (select_dp->T[i] == 2)   /* L変数には予想される選択リスト実値の幅が入る */
            if (scale > 0)   /* NUNBERで小数(プラス)の場合は小数第2位までの分3文字加算 */
                printf ("%.*s(%d) ", select_dp->L[i]+3, title,select_dp->L[i]+3);
            else             /* NUMBERで整数(マイナス)の場合 */
                printf ("%.*s(%d) ", select_dp->L[i], title,select_dp->L[i]);
        else                /* NUMBER以外の場合(ここでも右スペース埋めしている?) */
            printf("%-.*s(%d) ", select_dp->L[i], title,select_dp->L[i]);

        /* LONG_ROWかNUMBER以外の場合は文字出力する為にVARCHAR2に再設定(外部データ型)する */
        if (select_dp->T[i] != 24 && select_dp->T[i] != 2) select_dp->T[i] = 1;

        /* NUMBERは小数か整数かfloatかintに再設定(外部データ型)する */
        if (select_dp->T[i] == 2)
            if (scale > 0)
                select_dp->T[i] = 4;  /* 小数の場合はfloat */
            else
                select_dp->T[i] = 3;  /* 整数の場合はint */
    }
    printf ("\n\n");

    /* 対象レコードを読みきったらend_select_loopへ */
    EXEC SQL WHENEVER NOT FOUND GOTO end_select_loop;

    /* レコード内容の表示 */
    for (;;)
    {   /* 1行フェッチする */
        EXEC SQL FETCH C USING DESCRIPTOR select_dp;

        /* 選択リスト項目数分ループ */
        for (i = 0; i < select_dp->F; i++)
        {   /* 値がNULLの場合は予想桁数分スペース出力 */
            if (*select_dp->I[i] < 0)
                if (select_dp->T[i] == 4)
                    printf ("%-*c ",(int)select_dp->L[i]+3, ' ');
                else
                    printf ("%-*c ",(int)select_dp->L[i], ' ');
            else    /* NULLでない場合 */
                if (select_dp->T[i] == 3)       /* intの場合 */
                    printf ("%*d ", (int)select_dp->L[i], *(int *)select_dp->V[i]);
                else if (select_dp->T[i] == 4)  /* floatの場合は小数第2位まで表示 */
                    printf ("%*.2f ", (int)select_dp->L[i], *(float *)select_dp->V[i]);
                else                            /* 以外(文字列)の場合 */
                    printf ("%-*.*s ", (int)select_dp->L[i], (int)select_dp->L[i], select_dp->V[i]);
        }
        printf ("\n");
    }
end_select_loop:
    return;
}

/* HELPコマンドが支持された場合のメッセージ出力 */
void help()
{
    puts("\n\n『SQL>』プロンプトからSQLかPL/SQLブロックを入力して下さい。");
    puts("ステートメントは複数行に渡って記述することが出来ます。");
    puts("ステートメントの最後はセミコロンを入力して下さい。");
    puts("PL/SQLブロックの終了はスラッシュを入力して下さい。");
    puts("『exit』でプログラムを終了します。\n");
    puts("『?』か『help』で本メッセージを表\示します。.\n\n");
}

/* SQLエラー発生時処理 */
void sql_error()
{
    /* ORACLE error handler */
    printf ("\n\n%.70s\n",sqlca.sqlerrm.sqlerrmc);
    if (parse_flag)
    {   /* SQL構文解析でエラーが発生した場合 */
        printf("構\文解析に失敗しました。(%d文字目)\n%s\n", sqlca.sqlerrd[4], dyn_statement);
    }
    EXEC SQL WHENEVER SQLERROR CONTINUE;
    EXEC SQL ROLLBACK WORK;     /* ロールバック */
    longjmp(jmp_continue, 1);   /* setjmp位置へ強制移動 */
}

関連記事

関連記事はありません。

 

コメントとトラックバック

    コメントはありません

    コメントを残す

    CAPTCHA


    トラックバックURLhttps://e-tune-mt.net/ylab/1196/trackback