Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ All notable changes to plx are recorded here. The format follows
[Keep a Changelog](https://keepachangelog.com/), and plx uses the extension
version in `plx.control` (currently `1.0`).

## [Unreleased]

### Fixed

- `.to_s`, `.to_i` and `.to_f` were rewritten as casts wherever the text
appeared, so a field whose name starts with one of them broke:
`r.to_status` became `r::texttatus`. They are now rewritten only as whole
words, and `x.to_s()` with an empty argument list works too.

## [2.0.1] - 2026-08-25

Packaging only. **The extension is unchanged at 2.0.0**, so there is no
Expand Down
22 changes: 16 additions & 6 deletions src/plx_transpile.c
Original file line number Diff line number Diff line change
Expand Up @@ -1379,14 +1379,24 @@ rewrite_expr_inner(Ctx *cx, const char *s, int len, bool boolctx)
}
continue;
}
/* .to_i / .to_s / .to_f casts */
if (c == '.' && i + 4 <= len && strncmp(s + i, ".to_", 4) == 0)
/*
* .to_i / .to_s / .to_f casts, as whole words only: a field such as
* r.to_status or r.to_s_name is left alone. An empty argument list,
* x.to_s(), is dropped with the method name.
*/
if (c == '.' && i + 5 <= len && strncmp(s + i, ".to_", 4) == 0 &&
(s[i + 4] == 'i' || s[i + 4] == 's' || s[i + 4] == 'f') &&
!(i + 5 < len && (isalnum((unsigned char) s[i + 5]) || s[i + 5] == '_' ||
s[i + 5] == '$' || IS_HIGHBIT_SET(s[i + 5]))))
{
char k = (i + 4 < len) ? s[i + 4] : '?';
char k = s[i + 4];

if (k == 'i') { appendStringInfoString(&out, "::integer"); i += 5; continue; }
if (k == 's') { appendStringInfoString(&out, "::text"); i += 5; continue; }
if (k == 'f') { appendStringInfoString(&out, "::double precision"); i += 5; continue; }
appendStringInfoString(&out, k == 'i' ? "::integer" :
k == 's' ? "::text" : "::double precision");
i += 5;
if (i + 1 < len && s[i] == '(' && s[i + 1] == ')')
i += 2;
continue;
}
/* operator runs */
if (strchr("=!<>+-*/%&|?:~^", c))
Expand Down
13 changes: 13 additions & 0 deletions test/expected/plx_features.out
Original file line number Diff line number Diff line change
Expand Up @@ -233,3 +233,16 @@ SELECT f_strict() AS strict_no_row;
-1
(1 row)

-- .to_s / .to_i / .to_f are rewritten as whole words only, so a field whose
-- name starts with to_s, to_i or to_f is left alone; x.to_s() also works
CREATE FUNCTION f_to_cast(n int) RETURNS text LANGUAGE plxruby AS $$
r = fetch_one("select 'shipped' as to_status, 'x' as to_s_name, 2 as to_items")
return "#{r.to_status}/#{r.to_s_name}/#{r.to_items}/#{n.to_s}/#{n.to_s()}/#{'4.5'.to_f}/#{'12'.to_i + 1}"
$$;
SELECT f_to_cast(3);
f_to_cast
------------------------
shipped/x/2/3/3/4.5/13
(1 row)

DROP FUNCTION f_to_cast(int);
9 changes: 9 additions & 0 deletions test/sql/plx_features.sql
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,12 @@ rescue => e
end
$$;
SELECT f_strict() AS strict_no_row;

-- .to_s / .to_i / .to_f are rewritten as whole words only, so a field whose
-- name starts with to_s, to_i or to_f is left alone; x.to_s() also works
CREATE FUNCTION f_to_cast(n int) RETURNS text LANGUAGE plxruby AS $$
r = fetch_one("select 'shipped' as to_status, 'x' as to_s_name, 2 as to_items")
return "#{r.to_status}/#{r.to_s_name}/#{r.to_items}/#{n.to_s}/#{n.to_s()}/#{'4.5'.to_f}/#{'12'.to_i + 1}"
$$;
SELECT f_to_cast(3);
DROP FUNCTION f_to_cast(int);