Skip to content

Commit dc1158d

Browse files
committed
Add ODBC compatibility features; pass 23/61 psqlodbc regression tests
Implements the features exposed by running the original psqlodbc test suite against psqlodbc2 via the unixODBC driver manager: - Parameter marker translation (? -> $N) with quote/comment awareness and ODBC escape clauses ({fn}, {d}, {t}, {ts}, {oj}, {escape}) - PQdescribePrepared for result metadata after SQLPrepare - SQLColAttribute, SQLGetInfo, SQLNumParams, SQLNativeSql - SQL_C_INTERVAL type conversion - Boolean type handling and BoolsAsChar connection option - Encoding-aware SQL_DESC_OCTET_LENGTH, interval type names - Parameter type casts ($1::int2) and char<->binary cross-binding - NOTICE capture via libpq notice receiver - Rich SQLSTATE error messages with severity prefix and context - Autocommit/transaction recovery (ROLLBACK on failed txn) Adds regress/run_regression.sh to build and run the original psqlodbc test programs against the built driver.
1 parent b2605f4 commit dc1158d

19 files changed

Lines changed: 3321 additions & 107 deletions

psqlodbc2.def

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,9 @@ EXPORTS
2828
SQLBindCol @26
2929
SQLSetStmtAttr @27
3030
SQLGetStmtAttr @28
31+
SQLSetEnvAttr @29
32+
SQLGetEnvAttr @30
33+
SQLColAttribute @31
34+
SQLGetInfo @32
35+
SQLNumParams @33
36+
SQLNativeSql @34

regress/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Generated regression test artifacts: compiled test binaries, generated
2+
# odbc.ini/odbcinst.ini, and captured results. Only run_regression.sh is tracked.
3+
work/

regress/run_regression.sh

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
#!/bin/bash
2+
#
3+
# Run the original psqlodbc regression tests against psqlodbc2.
4+
#
5+
# This script:
6+
# 1. Creates a local odbcinst.ini registering our driver
7+
# 2. Creates a local odbc.ini with a test DSN pointing to the local PG
8+
# 3. Compiles the original test programs from ~/projects/psqlodbc/test/src
9+
# 4. Loads the sample tables
10+
# 5. Runs each test and compares output against expected results
11+
#
12+
# Prerequisites:
13+
# - psqlodbc2 built in ../builddir/
14+
# - PostgreSQL running on localhost:5432
15+
# - A database named contrib_regression accessible by current user
16+
# - The original psqlodbc source at ~/projects/psqlodbc
17+
#
18+
# Usage:
19+
# ./run_regression.sh [test_name ...]
20+
# If no test names given, runs all tests listed in the 'tests' file.
21+
22+
set -e
23+
24+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
25+
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
26+
ORIG_TEST_DIR="${HOME}/projects/psqlodbc/test"
27+
BUILD_DIR="${PROJECT_DIR}/builddir"
28+
29+
# Configurable via environment
30+
PG_HOST="${PG_HOST:-localhost}"
31+
PG_PORT="${PG_PORT:-5432}"
32+
PG_DATABASE="${PG_DATABASE:-contrib_regression}"
33+
PG_USER="${PG_USER:-$(whoami)}"
34+
35+
# Verify prerequisites
36+
if [ ! -d "$BUILD_DIR/src" ]; then
37+
echo "ERROR: Build directory not found. Run 'meson compile -C builddir' first."
38+
exit 1
39+
fi
40+
41+
if [ ! -d "$ORIG_TEST_DIR/src" ]; then
42+
echo "ERROR: Original psqlodbc test sources not found at $ORIG_TEST_DIR"
43+
exit 1
44+
fi
45+
46+
# Find the built driver shared library
47+
DRIVER_LIB=""
48+
for ext in so dylib dll; do
49+
if [ -f "$BUILD_DIR/src/libpsqlodbc2w.$ext" ]; then
50+
DRIVER_LIB="$BUILD_DIR/src/libpsqlodbc2w.$ext"
51+
break
52+
fi
53+
done
54+
55+
if [ -z "$DRIVER_LIB" ]; then
56+
echo "ERROR: Cannot find built driver library in $BUILD_DIR/src/"
57+
exit 1
58+
fi
59+
60+
echo "Driver: $DRIVER_LIB"
61+
echo "Database: $PG_HOST:$PG_PORT/$PG_DATABASE"
62+
63+
# Set up working directory
64+
WORK_DIR="$SCRIPT_DIR/work"
65+
mkdir -p "$WORK_DIR/exe" "$WORK_DIR/results"
66+
67+
# Create odbcinst.ini
68+
cat > "$WORK_DIR/odbcinst.ini" << EOF
69+
[ODBC]
70+
Trace = off
71+
TraceFile =
72+
73+
[psqlodbc2]
74+
Description = psqlodbc2 driver under test
75+
Driver = $DRIVER_LIB
76+
EOF
77+
78+
# Create odbc.ini
79+
cat > "$WORK_DIR/odbc.ini" << EOF
80+
[psqlodbc_test_dsn]
81+
Description = psqlodbc2 regression test DSN
82+
Driver = psqlodbc2
83+
Trace = No
84+
TraceFile =
85+
Database = $PG_DATABASE
86+
Servername = $PG_HOST
87+
Username = $PG_USER
88+
Password =
89+
Port = $PG_PORT
90+
ReadOnly = No
91+
ConnSettings = set lc_messages='C'
92+
93+
[psqlodbc_test_dsn_ansi]
94+
Description = psqlodbc2 regression test DSN (ansi)
95+
Driver = psqlodbc2
96+
Trace = No
97+
TraceFile =
98+
Database = $PG_DATABASE
99+
Servername = $PG_HOST
100+
Username = $PG_USER
101+
Password =
102+
Port = $PG_PORT
103+
ReadOnly = No
104+
ConnSettings = set lc_messages='C'
105+
EOF
106+
107+
# Export ODBC environment to use our local configs.
108+
# ODBCSYSINI is the directory containing odbcinst.ini (not the file path).
109+
# ODBCINI is the full path to the odbc.ini file.
110+
export ODBCSYSINI="$WORK_DIR"
111+
export ODBCINI="$WORK_DIR/odbc.ini"
112+
113+
# Determine compiler flags
114+
ODBC_CFLAGS=$(pkg-config --cflags odbc 2>/dev/null || echo "-I/usr/include")
115+
ODBC_LIBS=$(pkg-config --libs odbc 2>/dev/null || echo "-lodbc")
116+
PG_CFLAGS=$(pg_config --includedir 2>/dev/null | xargs -I{} echo "-I{}" || echo "")
117+
118+
CFLAGS="-g -O0 -Wall $ODBC_CFLAGS $PG_CFLAGS -I$ORIG_TEST_DIR/.."
119+
LDFLAGS="$ODBC_LIBS"
120+
121+
# Compile common.o
122+
echo "Compiling test harness..."
123+
cc $CFLAGS -c "$ORIG_TEST_DIR/src/common.c" -o "$WORK_DIR/exe/common.o" 2>/dev/null || {
124+
# If config.h is needed, create a minimal one
125+
echo "#define HAVE_STDBOOL_H 1" > "$WORK_DIR/config.h"
126+
cc $CFLAGS -I"$WORK_DIR" -c "$ORIG_TEST_DIR/src/common.c" -o "$WORK_DIR/exe/common.o"
127+
}
128+
129+
# Load sample tables
130+
echo "Loading sample tables..."
131+
psql -h "$PG_HOST" -p "$PG_PORT" -d "$PG_DATABASE" -q -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" 2>/dev/null || true
132+
psql -h "$PG_HOST" -p "$PG_PORT" -d "$PG_DATABASE" -q -f "$ORIG_TEST_DIR/sampletables.sql" 2>/dev/null
133+
134+
# Determine which tests to run
135+
if [ $# -gt 0 ]; then
136+
TESTS="$@"
137+
else
138+
# Start with a subset of tests most likely to work with our current feature set
139+
TESTS="connect select stmthandles update commands getresult prepare params"
140+
fi
141+
142+
echo ""
143+
echo "Running regression tests..."
144+
echo "=============================="
145+
146+
PASS=0
147+
FAIL=0
148+
SKIP=0
149+
150+
for test_name in $TESTS; do
151+
src_file="$ORIG_TEST_DIR/src/${test_name}-test.c"
152+
if [ ! -f "$src_file" ]; then
153+
echo "SKIP: $test_name (source not found)"
154+
SKIP=$((SKIP + 1))
155+
continue
156+
fi
157+
158+
# Compile the test
159+
exe_file="$WORK_DIR/exe/${test_name}-test"
160+
if ! cc $CFLAGS -I"$WORK_DIR" "$src_file" "$WORK_DIR/exe/common.o" -o "$exe_file" $LDFLAGS 2>/dev/null; then
161+
echo "SKIP: $test_name (compilation failed)"
162+
SKIP=$((SKIP + 1))
163+
continue
164+
fi
165+
166+
# Run the test and capture output
167+
result_file="$WORK_DIR/results/${test_name}.out"
168+
if ! "$exe_file" > "$result_file" 2>&1; then
169+
# Test crashed or returned non-zero
170+
echo "FAIL: $test_name (crashed or returned error)"
171+
if [ -s "$result_file" ]; then
172+
echo " Output: $(head -5 "$result_file" | tr '\n' ' ')"
173+
fi
174+
FAIL=$((FAIL + 1))
175+
continue
176+
fi
177+
178+
# Compare against expected output
179+
expected_file="$ORIG_TEST_DIR/expected/${test_name}.out"
180+
if [ ! -f "$expected_file" ]; then
181+
echo "PASS: $test_name (no expected file to compare)"
182+
PASS=$((PASS + 1))
183+
continue
184+
fi
185+
186+
if diff -q "$expected_file" "$result_file" >/dev/null 2>&1; then
187+
echo "PASS: $test_name"
188+
PASS=$((PASS + 1))
189+
else
190+
echo "FAIL: $test_name (output differs)"
191+
diff -u "$expected_file" "$result_file" | head -20
192+
echo " ..."
193+
FAIL=$((FAIL + 1))
194+
fi
195+
done
196+
197+
echo ""
198+
echo "=============================="
199+
echo "Results: $PASS passed, $FAIL failed, $SKIP skipped"
200+
echo ""
201+
202+
if [ $FAIL -gt 0 ]; then
203+
exit 1
204+
fi
205+
exit 0

src/connection.c

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,49 @@
2020
#include <string.h>
2121
#include <stdio.h>
2222

23+
/*
24+
* Return the maximum number of bytes a single character can occupy in the
25+
* given PostgreSQL client encoding name. Used to translate a column's
26+
* character-count size into a worst-case octet (byte) length.
27+
*
28+
* Only the multibyte encodings that differ from 1 are enumerated; anything
29+
* unrecognized (including all single-byte encodings such as LATIN1 and SQL_ASCII)
30+
* defaults to 1. Values match PostgreSQL's pg_wchar.h maxmblen table.
31+
*/
32+
static int max_bytes_per_char_for_encoding(const char *encoding_name)
33+
{
34+
if (!encoding_name) {
35+
return 1;
36+
}
37+
/* UTF-8 is by far the common case and allows up to 4 bytes per character. */
38+
if (strcmp(encoding_name, "UTF8") == 0) {
39+
return 4;
40+
}
41+
/* Encodings that use up to 4 bytes per character. */
42+
if (strcmp(encoding_name, "GB18030") == 0) {
43+
return 4;
44+
}
45+
/* Encodings that use up to 3 bytes per character. */
46+
if (strcmp(encoding_name, "EUC_JP") == 0 ||
47+
strcmp(encoding_name, "EUC_CN") == 0 ||
48+
strcmp(encoding_name, "EUC_KR") == 0 ||
49+
strcmp(encoding_name, "EUC_TW") == 0 ||
50+
strcmp(encoding_name, "EUC_JIS_2004") == 0 ||
51+
strcmp(encoding_name, "MULE_INTERNAL") == 0) {
52+
return 3;
53+
}
54+
/* Encodings that use up to 2 bytes per character. */
55+
if (strcmp(encoding_name, "SJIS") == 0 ||
56+
strcmp(encoding_name, "BIG5") == 0 ||
57+
strcmp(encoding_name, "GBK") == 0 ||
58+
strcmp(encoding_name, "UHC") == 0 ||
59+
strcmp(encoding_name, "JOHAB") == 0 ||
60+
strcmp(encoding_name, "SHIFT_JIS_2004") == 0) {
61+
return 2;
62+
}
63+
return 1;
64+
}
65+
2366
SQLRETURN connection_allocate(OdbcEnvironment *environment, SQLHANDLE *output_handle)
2467
{
2568
if (!environment || !output_handle) {
@@ -94,6 +137,7 @@ SQLRETURN connection_free(SQLHANDLE handle)
94137

95138
/* Clean up all resources */
96139
diagnostics_clear(&connection->diagnostics);
140+
connection_clear_notices(connection);
97141
connection_info_clear(&connection->info);
98142

99143
/* Poison the magic number to detect use-after-free */
@@ -103,6 +147,63 @@ SQLRETURN connection_free(SQLHANDLE handle)
103147
return SQL_SUCCESS;
104148
}
105149

150+
void connection_clear_notices(OdbcConnection *connection)
151+
{
152+
if (!connection) {
153+
return;
154+
}
155+
for (int i = 0; i < connection->notice_count; i++) {
156+
free(connection->captured_notices[i]);
157+
connection->captured_notices[i] = NULL;
158+
}
159+
connection->notice_count = 0;
160+
}
161+
162+
/*
163+
* libpq notice receiver callback. Called by libpq whenever PostgreSQL sends
164+
* a NOTICE or WARNING message (e.g., "table already exists", implicit index
165+
* creation, etc.). We extract the message and store it on the connection for
166+
* later promotion to ODBC diagnostic records.
167+
*
168+
* We construct the message in the format "SEVERITY: primary_message" to match
169+
* the original psqlodbc driver's behavior.
170+
*/
171+
static void notice_receiver_callback(void *context, const PGresult *notice_result)
172+
{
173+
OdbcConnection *connection = (OdbcConnection *)context;
174+
if (!connection || connection->notice_count >= MAX_CAPTURED_NOTICES) {
175+
return;
176+
}
177+
178+
/* Extract individual message fields for clean formatting */
179+
const char *severity = PQresultErrorField(notice_result, PG_DIAG_SEVERITY);
180+
const char *primary = PQresultErrorField(notice_result, PG_DIAG_MESSAGE_PRIMARY);
181+
182+
if (!primary || primary[0] == '\0') {
183+
return;
184+
}
185+
186+
/* Format: "NOTICE: message text" (single space after colon) */
187+
const char *sev = severity ? severity : "NOTICE";
188+
size_t sev_len = strlen(sev);
189+
size_t msg_len = strlen(primary);
190+
/* "SEVERITY: message\0" */
191+
size_t total_len = sev_len + 2 + msg_len;
192+
193+
char *copy = malloc(total_len + 1);
194+
if (!copy) {
195+
return;
196+
}
197+
memcpy(copy, sev, sev_len);
198+
copy[sev_len] = ':';
199+
copy[sev_len + 1] = ' ';
200+
memcpy(copy + sev_len + 2, primary, msg_len);
201+
copy[total_len] = '\0';
202+
203+
connection->captured_notices[connection->notice_count] = copy;
204+
connection->notice_count++;
205+
}
206+
106207
SQLRETURN connection_connect(OdbcConnection *connection)
107208
{
108209
if (!connection) {
@@ -149,13 +250,22 @@ SQLRETURN connection_connect(OdbcConnection *connection)
149250

150251
connection->state = CONNECTION_STATE_CONNECTED;
151252

253+
/* Install notice receiver to capture NOTICE/WARNING messages from PostgreSQL.
254+
* These are promoted to ODBC diagnostic records after statement execution. */
255+
PQsetNoticeReceiver(connection->libpq_connection, notice_receiver_callback, connection);
256+
152257
/* Parse the server version for feature detection.
153258
* PQserverVersion returns an integer like 150002 for 15.0.2
154259
* (major * 10000 + minor * 100 + patch). */
155260
int version_number = PQserverVersion(connection->libpq_connection);
156261
connection->server_version_major = version_number / 10000;
157262
connection->server_version_minor = (version_number / 100) % 100;
158263

264+
/* Determine the worst-case bytes-per-character for the negotiated client
265+
* encoding, used when reporting octet lengths of character columns. */
266+
connection->max_bytes_per_char =
267+
max_bytes_per_char_for_encoding(pg_encoding_to_char(PQclientEncoding(connection->libpq_connection)));
268+
159269
return SQL_SUCCESS;
160270
}
161271

@@ -210,6 +320,13 @@ void connection_info_clear(ConnectionInfo *info)
210320
memset(info->sslmode, 0, sizeof(info->sslmode));
211321
memset(info->application_name, 0, sizeof(info->application_name));
212322
info->connect_timeout = 0;
323+
324+
/* BoolsAsChar defaults to on, matching the original psqlodbc driver. */
325+
info->bools_as_char = true;
326+
327+
/* Size-reporting defaults match the original psqlodbc driver. */
328+
info->unknown_sizes = UNKNOWN_SIZES_MAX;
329+
info->max_varchar_size = DEFAULT_MAX_VARCHAR_SIZE;
213330
}
214331

215332
bool connection_add_statement(OdbcConnection *connection,

0 commit comments

Comments
 (0)