From b3ffc2bdcbc8b507e7041d022c31b81f5fd74bde Mon Sep 17 00:00:00 2001 From: guoyangzhen Date: Mon, 16 Mar 2026 12:20:54 +0800 Subject: [PATCH 1/3] fix(postgresql): only replace ? param placeholders outside quoted strings and comments Fixes #216 Fixes nuxt/content#3682 The previous normalizeParams used a naive sql.replace(/\?/g, ...) that replaced ALL question marks, including literal ones inside: - Single-quoted strings (e.g., 'What?') - Dollar-quoted strings (e.g., 397449text397449) - Comments This caused data corruption when messages contained question marks. The fix uses proper string-aware parsing to only replace ? characters that appear outside of quoted regions. --- src/monitor/wechat_listener.py | 192 +++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 src/monitor/wechat_listener.py diff --git a/src/monitor/wechat_listener.py b/src/monitor/wechat_listener.py new file mode 100644 index 00000000..27a88923 --- /dev/null +++ b/src/monitor/wechat_listener.py @@ -0,0 +1,192 @@ +import pg from "pg"; + +import type { Connector, Primitive } from "db0"; + +import { BoundableStatement } from "./_internal/statement.ts"; + +export type ConnectorOptions = { url: string } | pg.ClientConfig; + +type InternalQuery = ( + sql: string, + params?: Primitive[], +) => Promise; + +export default function postgresqlConnector( + opts: ConnectorOptions, +): Connector { + let _client: undefined | pg.Client | Promise; + function getClient() { + if (_client) { + return _client; + } + const client = new pg.Client("url" in opts ? opts.url : opts); + _client = client.connect().then(() => { + _client = client; + return _client; + }); + return _client; + } + + const query: InternalQuery = async (sql, params) => { + const client = await getClient(); + return client.query(normalizeParams(sql), params); + }; + + return { + name: "postgresql", + dialect: "postgresql", + getInstance: () => getClient(), + exec: (sql) => query(sql), + prepare: (sql) => new StatementWrapper(sql, query), + dispose: async () => { + await (await _client)?.end?.(); + _client = undefined; + }, + }; +} + +/** + * Replace `?` placeholders with PostgreSQL `$N` positional parameters, + * while preserving literal `?` inside quoted strings and comments. + * + * Handles: + * - Single-quoted strings (including escaped `''`) + * - Dollar-quoted strings (`$$...$$` and `$tag$...$tag$`) + * - Double-quoted identifiers + * - Line comments (`--`) + * - Block comments (`/* */`) + */ +function normalizeParams(sql: string) { + const result: string[] = []; + let i = 0; + let paramIdx = 0; + const n = sql.length; + + while (i < n) { + // Single-quoted string + if (sql[i] === "'") { + let j = i + 1; + while (j < n) { + if (sql[j] === "'") { + if (j + 1 < n && sql[j + 1] === "'") { + j += 2; // escaped quote '' + continue; + } + j++; + break; + } + j++; + } + result.push(sql.slice(i, j)); + i = j; + continue; + } + + // Dollar-quoted string (PostgreSQL): $tag$...$tag$ or $$...$$ + if ( + sql[i] === "$" && + i + 1 < n && + (sql[i + 1] === "$" || /[a-zA-Z_]/.test(sql[i + 1])) + ) { + let j = i + 1; + if (sql[j] !== "$") { + while (j < n && /[a-zA-Z0-9_]/.test(sql[j])) j++; + if (j >= n || sql[j] !== "$") { + result.push(sql[i]); + i++; + continue; + } + } + const tag = sql.slice(i, j + 1); + const end = sql.indexOf(tag, j + 1); + if (end === -1) { + result.push(sql.slice(i)); + break; + } + result.push(sql.slice(i, end + tag.length)); + i = end + tag.length; + continue; + } + + // Double-quoted identifier + if (sql[i] === '"') { + let j = i + 1; + while (j < n) { + if (sql[j] === '"') { + j++; + break; + } + j++; + } + result.push(sql.slice(i, j)); + i = j; + continue; + } + + // Line comment -- + if (i + 1 < n && sql[i] === "-" && sql[i + 1] === "-") { + const j = sql.indexOf("\n", i); + if (j === -1) { + result.push(sql.slice(i)); + break; + } + result.push(sql.slice(i, j)); + i = j; + continue; + } + + // Block comment /* */ + if (i + 1 < n && sql[i] === "/" && sql[i + 1] === "*") { + const end = sql.indexOf("*/", i + 2); + if (end === -1) { + result.push(sql.slice(i)); + break; + } + result.push(sql.slice(i, end + 2)); + i = end + 2; + continue; + } + + // Parameter placeholder ? + if (sql[i] === "?") { + paramIdx++; + result.push(`$${paramIdx}`); + i++; + continue; + } + + result.push(sql[i]); + i++; + } + + return result.join(""); +} + +class StatementWrapper extends BoundableStatement { + #query: InternalQuery; + #sql: string; + + constructor(sql: string, query: InternalQuery) { + super(); + this.#sql = sql; + this.#query = query; + } + + async all(...params: Primitive[]) { + const res = await this.#query(this.#sql, params); + return res.rows; + } + + async run(...params: Primitive[]) { + const res = await this.#query(this.#sql, params); + return { + success: true, + ...res, + }; + } + + async get(...params: Primitive[]) { + const res = await this.#query(this.#sql, params); + return res.rows[0]; + } +} From 97c7a28979c4d59b56880a484052841c441ad8a7 Mon Sep 17 00:00:00 2001 From: guoyangzhen Date: Mon, 16 Mar 2026 14:57:08 +0800 Subject: [PATCH 2/3] remove incorrectly committed file --- src/monitor/wechat_listener.py | 192 --------------------------------- 1 file changed, 192 deletions(-) delete mode 100644 src/monitor/wechat_listener.py diff --git a/src/monitor/wechat_listener.py b/src/monitor/wechat_listener.py deleted file mode 100644 index 27a88923..00000000 --- a/src/monitor/wechat_listener.py +++ /dev/null @@ -1,192 +0,0 @@ -import pg from "pg"; - -import type { Connector, Primitive } from "db0"; - -import { BoundableStatement } from "./_internal/statement.ts"; - -export type ConnectorOptions = { url: string } | pg.ClientConfig; - -type InternalQuery = ( - sql: string, - params?: Primitive[], -) => Promise; - -export default function postgresqlConnector( - opts: ConnectorOptions, -): Connector { - let _client: undefined | pg.Client | Promise; - function getClient() { - if (_client) { - return _client; - } - const client = new pg.Client("url" in opts ? opts.url : opts); - _client = client.connect().then(() => { - _client = client; - return _client; - }); - return _client; - } - - const query: InternalQuery = async (sql, params) => { - const client = await getClient(); - return client.query(normalizeParams(sql), params); - }; - - return { - name: "postgresql", - dialect: "postgresql", - getInstance: () => getClient(), - exec: (sql) => query(sql), - prepare: (sql) => new StatementWrapper(sql, query), - dispose: async () => { - await (await _client)?.end?.(); - _client = undefined; - }, - }; -} - -/** - * Replace `?` placeholders with PostgreSQL `$N` positional parameters, - * while preserving literal `?` inside quoted strings and comments. - * - * Handles: - * - Single-quoted strings (including escaped `''`) - * - Dollar-quoted strings (`$$...$$` and `$tag$...$tag$`) - * - Double-quoted identifiers - * - Line comments (`--`) - * - Block comments (`/* */`) - */ -function normalizeParams(sql: string) { - const result: string[] = []; - let i = 0; - let paramIdx = 0; - const n = sql.length; - - while (i < n) { - // Single-quoted string - if (sql[i] === "'") { - let j = i + 1; - while (j < n) { - if (sql[j] === "'") { - if (j + 1 < n && sql[j + 1] === "'") { - j += 2; // escaped quote '' - continue; - } - j++; - break; - } - j++; - } - result.push(sql.slice(i, j)); - i = j; - continue; - } - - // Dollar-quoted string (PostgreSQL): $tag$...$tag$ or $$...$$ - if ( - sql[i] === "$" && - i + 1 < n && - (sql[i + 1] === "$" || /[a-zA-Z_]/.test(sql[i + 1])) - ) { - let j = i + 1; - if (sql[j] !== "$") { - while (j < n && /[a-zA-Z0-9_]/.test(sql[j])) j++; - if (j >= n || sql[j] !== "$") { - result.push(sql[i]); - i++; - continue; - } - } - const tag = sql.slice(i, j + 1); - const end = sql.indexOf(tag, j + 1); - if (end === -1) { - result.push(sql.slice(i)); - break; - } - result.push(sql.slice(i, end + tag.length)); - i = end + tag.length; - continue; - } - - // Double-quoted identifier - if (sql[i] === '"') { - let j = i + 1; - while (j < n) { - if (sql[j] === '"') { - j++; - break; - } - j++; - } - result.push(sql.slice(i, j)); - i = j; - continue; - } - - // Line comment -- - if (i + 1 < n && sql[i] === "-" && sql[i + 1] === "-") { - const j = sql.indexOf("\n", i); - if (j === -1) { - result.push(sql.slice(i)); - break; - } - result.push(sql.slice(i, j)); - i = j; - continue; - } - - // Block comment /* */ - if (i + 1 < n && sql[i] === "/" && sql[i + 1] === "*") { - const end = sql.indexOf("*/", i + 2); - if (end === -1) { - result.push(sql.slice(i)); - break; - } - result.push(sql.slice(i, end + 2)); - i = end + 2; - continue; - } - - // Parameter placeholder ? - if (sql[i] === "?") { - paramIdx++; - result.push(`$${paramIdx}`); - i++; - continue; - } - - result.push(sql[i]); - i++; - } - - return result.join(""); -} - -class StatementWrapper extends BoundableStatement { - #query: InternalQuery; - #sql: string; - - constructor(sql: string, query: InternalQuery) { - super(); - this.#sql = sql; - this.#query = query; - } - - async all(...params: Primitive[]) { - const res = await this.#query(this.#sql, params); - return res.rows; - } - - async run(...params: Primitive[]) { - const res = await this.#query(this.#sql, params); - return { - success: true, - ...res, - }; - } - - async get(...params: Primitive[]) { - const res = await this.#query(this.#sql, params); - return res.rows[0]; - } -} From 0243cbe9d62a1a4fbc5cf4e8467cce4cbab4a4e2 Mon Sep 17 00:00:00 2001 From: guoyangzhen Date: Mon, 16 Mar 2026 14:57:28 +0800 Subject: [PATCH 3/3] fix(postgresql): only replace ? param placeholders outside quoted strings Fixes #216 Fixes nuxt/content#3682 --- src/connectors/postgresql.ts | 115 ++++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 2 deletions(-) diff --git a/src/connectors/postgresql.ts b/src/connectors/postgresql.ts index c7457be8..27a88923 100644 --- a/src/connectors/postgresql.ts +++ b/src/connectors/postgresql.ts @@ -45,10 +45,121 @@ export default function postgresqlConnector( }; } -// https://www.postgresql.org/docs/9.3/sql-prepare.html +/** + * Replace `?` placeholders with PostgreSQL `$N` positional parameters, + * while preserving literal `?` inside quoted strings and comments. + * + * Handles: + * - Single-quoted strings (including escaped `''`) + * - Dollar-quoted strings (`$$...$$` and `$tag$...$tag$`) + * - Double-quoted identifiers + * - Line comments (`--`) + * - Block comments (`/* */`) + */ function normalizeParams(sql: string) { + const result: string[] = []; let i = 0; - return sql.replace(/\?/g, () => `$${++i}`); + let paramIdx = 0; + const n = sql.length; + + while (i < n) { + // Single-quoted string + if (sql[i] === "'") { + let j = i + 1; + while (j < n) { + if (sql[j] === "'") { + if (j + 1 < n && sql[j + 1] === "'") { + j += 2; // escaped quote '' + continue; + } + j++; + break; + } + j++; + } + result.push(sql.slice(i, j)); + i = j; + continue; + } + + // Dollar-quoted string (PostgreSQL): $tag$...$tag$ or $$...$$ + if ( + sql[i] === "$" && + i + 1 < n && + (sql[i + 1] === "$" || /[a-zA-Z_]/.test(sql[i + 1])) + ) { + let j = i + 1; + if (sql[j] !== "$") { + while (j < n && /[a-zA-Z0-9_]/.test(sql[j])) j++; + if (j >= n || sql[j] !== "$") { + result.push(sql[i]); + i++; + continue; + } + } + const tag = sql.slice(i, j + 1); + const end = sql.indexOf(tag, j + 1); + if (end === -1) { + result.push(sql.slice(i)); + break; + } + result.push(sql.slice(i, end + tag.length)); + i = end + tag.length; + continue; + } + + // Double-quoted identifier + if (sql[i] === '"') { + let j = i + 1; + while (j < n) { + if (sql[j] === '"') { + j++; + break; + } + j++; + } + result.push(sql.slice(i, j)); + i = j; + continue; + } + + // Line comment -- + if (i + 1 < n && sql[i] === "-" && sql[i + 1] === "-") { + const j = sql.indexOf("\n", i); + if (j === -1) { + result.push(sql.slice(i)); + break; + } + result.push(sql.slice(i, j)); + i = j; + continue; + } + + // Block comment /* */ + if (i + 1 < n && sql[i] === "/" && sql[i + 1] === "*") { + const end = sql.indexOf("*/", i + 2); + if (end === -1) { + result.push(sql.slice(i)); + break; + } + result.push(sql.slice(i, end + 2)); + i = end + 2; + continue; + } + + // Parameter placeholder ? + if (sql[i] === "?") { + paramIdx++; + result.push(`$${paramIdx}`); + i++; + continue; + } + + result.push(sql[i]); + i++; + } + + return result.join(""); } class StatementWrapper extends BoundableStatement {