Skip to content

Expose count_tokens() and add refresh_token_counts() helper#22

Open
syedkazim110 wants to merge 4 commits into
pgEdge:mainfrom
syedkazim110:feature/count-tokens-expose
Open

Expose count_tokens() and add refresh_token_counts() helper#22
syedkazim110 wants to merge 4 commits into
pgEdge:mainfrom
syedkazim110:feature/count-tokens-expose

Conversation

@syedkazim110

Copy link
Copy Markdown
Contributor

Add a SQL declaration for the existing C count_tokens() function so users can call pgedge_vectorizer.count_tokens(text) directly to inspect approximate token counts (~4 chars/token, UTF-8 aware).

Add refresh_token_counts(chunk_table regclass) to back-fill the token_count column on existing chunk rows without a full recreate_chunks() rebuild. Returns the number of rows updated.

Includes regression tests covering approximation values, NULL/UTF-8 handling, the refresh path, and schema-qualified chunk tables.

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@syedkazim110, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 56 minutes and 57 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e0a54ee4-db4d-4ae6-a867-d49088089380

📥 Commits

Reviewing files that changed from the base of the PR and between 0dd7f16 and 74f9073.

⛔ Files ignored due to path filters (1)
  • test/expected/count_tokens.out is excluded by !**/*.out
📒 Files selected for processing (5)
  • Makefile
  • sql/pgedge_vectorizer--1.0--1.1.sql
  • sql/pgedge_vectorizer--1.1.sql
  • src/tokenizer.c
  • test/sql/count_tokens.sql
📝 Walkthrough

Walkthrough

This PR adds token counting functionality to the pgedge_vectorizer extension. It introduces count_tokens(), a C-backed immutable function that performs approximate UTF-8 token counting, and refresh_token_counts(), a PL/pgSQL function that recomputes and updates token_count values for all rows in a specified chunk table. The changes include both migration and base schema SQL definitions, a comprehensive regression test covering function registration, correctness across multiple input types, and update semantics, and Makefile registration of the new test target.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and clearly summarizes the main change: exposing count_tokens() and adding refresh_token_counts() helper, which matches the primary objectives of the PR.
Description check ✅ Passed The description is directly related to the changeset, providing specific details about the two new functions (count_tokens and refresh_token_counts), their purposes, and the regression tests included.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codacy-production

codacy-production Bot commented May 28, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 duplication

Metric Results
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@syedkazim110

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
test/sql/count_tokens.sql (1)

72-75: ⚡ Quick win

Assert exact updated-row count instead of >= 1.

These checks are too weak for the stated contract (“returns number of rows updated”). Please assert equality with table row count so partial updates cannot pass silently.

Proposed test tightening
-SELECT pgedge_vectorizer.refresh_token_counts(
-    'count_tokens_refresh_test_content_chunks'::regclass
-) >= 1 AS refresh_returned_count;
+SELECT pgedge_vectorizer.refresh_token_counts(
+    'count_tokens_refresh_test_content_chunks'::regclass
+) = (
+    SELECT COUNT(*) FROM count_tokens_refresh_test_content_chunks
+) AS refresh_returned_count;
...
-SELECT pgedge_vectorizer.refresh_token_counts(
-    'count_tokens_ns_test.ns_chunks'::regclass
-) >= 1 AS refresh_returned_count;
+SELECT pgedge_vectorizer.refresh_token_counts(
+    'count_tokens_ns_test.ns_chunks'::regclass
+) = (
+    SELECT COUNT(*) FROM count_tokens_ns_test.ns_chunks
+) AS refresh_returned_count;

Also applies to: 108-110

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/sql/count_tokens.sql` around lines 72 - 75, Replace the weak >= 1
assertion with an exact equality check: call
pgedge_vectorizer.refresh_token_counts('count_tokens_refresh_test_content_chunks'::regclass)
and assert it equals the actual row count of the target table (e.g. compare to
(SELECT COUNT(*) FROM count_tokens_refresh_test_content_chunks)), so the test
enforces the exact number of rows updated; apply the same change for the other
occurrence around the pgedge_vectorizer.refresh_token_counts call on lines
~108-110.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sql/pgedge_vectorizer--1.0--1.1.sql`:
- Around line 994-1007: The refresh_token_counts function uses
pgedge_vectorizer.count_tokens(content) but the normal chunk write paths still
set token_count = length(chunk_text) / 4; update all chunk INSERT/UPDATE write
paths to compute token_count with pgedge_vectorizer.count_tokens(content) (i.e.,
replace occurrences assigning token_count from length(chunk_text)/4) so
inserted/updated rows use pgedge_vectorizer.count_tokens(content) consistently;
apply this change to every chunk write path in this migration (the INSERT/UPDATE
statements around the earlier referenced token_count assignments) and mirror the
same edits in the corresponding sql/pgedge_vectorizer--1.1.sql file.

---

Nitpick comments:
In `@test/sql/count_tokens.sql`:
- Around line 72-75: Replace the weak >= 1 assertion with an exact equality
check: call
pgedge_vectorizer.refresh_token_counts('count_tokens_refresh_test_content_chunks'::regclass)
and assert it equals the actual row count of the target table (e.g. compare to
(SELECT COUNT(*) FROM count_tokens_refresh_test_content_chunks)), so the test
enforces the exact number of rows updated; apply the same change for the other
occurrence around the pgedge_vectorizer.refresh_token_counts call on lines
~108-110.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 954868f4-7d14-46ba-b7bc-78efe6b85103

📥 Commits

Reviewing files that changed from the base of the PR and between 50faf42 and 53d23fd.

⛔ Files ignored due to path filters (1)
  • test/expected/count_tokens.out is excluded by !**/*.out
📒 Files selected for processing (4)
  • Makefile
  • sql/pgedge_vectorizer--1.0--1.1.sql
  • sql/pgedge_vectorizer--1.1.sql
  • test/sql/count_tokens.sql

Comment thread sql/pgedge_vectorizer--1.0--1.1.sql
@syedkazim110

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
sql/pgedge_vectorizer--1.1.sql (1)

1000-1004: ⚡ Quick win

Skip no-op rewrites in refresh_token_counts().

The UPDATE recomputes and rewrites token_count for every row, even those already correct. On a large chunk table this produces unnecessary dead tuples/WAL. Filtering on a real change also makes the returned ROW_COUNT reflect rows that actually changed, matching the "number of rows updated" wording in the comment.

♻️ Proposed change to avoid redundant writes
     EXECUTE format(
-        'UPDATE %s SET token_count = pgedge_vectorizer.count_tokens(content)',
+        'UPDATE %s SET token_count = pgedge_vectorizer.count_tokens(content)
+         WHERE token_count IS DISTINCT FROM pgedge_vectorizer.count_tokens(content)',
         p_chunk_table
     );

Note: this changes the return value to count only rows whose token_count actually changed. The Test 4 assertion (>= 1) still holds since all rows are forced to 0 beforehand. If callers rely on the count equaling total row count, keep the unconditional form.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sql/pgedge_vectorizer--1.1.sql` around lines 1000 - 1004, In
refresh_token_counts(), avoid rewriting unchanged rows by changing the EXECUTE
UPDATE that sets token_count via pgedge_vectorizer.count_tokens(content) to only
update rows where the computed value differs from the stored one (use "IS
DISTINCT FROM" or equivalent comparison against the computed token count); build
the dynamic SQL using p_chunk_table and the same
pgedge_vectorizer.count_tokens(content) expression in a WHERE clause (or JOIN to
a subquery that computes new_tokens) so GET DIAGNOSTICS rows_updated will
reflect only rows that actually changed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@sql/pgedge_vectorizer--1.1.sql`:
- Around line 1000-1004: In refresh_token_counts(), avoid rewriting unchanged
rows by changing the EXECUTE UPDATE that sets token_count via
pgedge_vectorizer.count_tokens(content) to only update rows where the computed
value differs from the stored one (use "IS DISTINCT FROM" or equivalent
comparison against the computed token count); build the dynamic SQL using
p_chunk_table and the same pgedge_vectorizer.count_tokens(content) expression in
a WHERE clause (or JOIN to a subquery that computes new_tokens) so GET
DIAGNOSTICS rows_updated will reflect only rows that actually changed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 06364c6c-0043-499d-a071-2233c9072810

📥 Commits

Reviewing files that changed from the base of the PR and between 53d23fd and d6ba4a3.

📒 Files selected for processing (2)
  • sql/pgedge_vectorizer--1.0--1.1.sql
  • sql/pgedge_vectorizer--1.1.sql
🚧 Files skipped from review as they are similar to previous changes (1)
  • sql/pgedge_vectorizer--1.0--1.1.sql

@syedkazim110

Copy link
Copy Markdown
Contributor Author

@davepage here's the count_tokens() SQL exposure + refresh_token_counts() helper you mentioned keeping from the closed tiktoken PR. No plpython3u, no GUC, no tiktoken dependency — just the two functions and 5 regression tests. Let me know if anything needs adjusting.

@dpage dpage left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

The refresh_token_counts() design and test coverage here are good, but there's a critical, merge-blocking defect: the PR declares a C function symbol that does not exist, which breaks not only the new functions but the core chunking pipeline after upgrade.

⚠️ Note: this wasn't caught by the green checks because the GitHub Actions CI job that runs make installcheck (.github/workflows/ci.yml:123) is in action_required state for this fork PR and never ran — only Codacy and CodeRabbit executed, neither of which builds/runs the extension (CodeRabbit also ignores .out files via the !**/*.out filter).


🔴 Must Fix — count_tokens() references a non-existent C symbol

Both sql/pgedge_vectorizer--1.1.sql:136 and sql/pgedge_vectorizer--1.0--1.1.sql:136 declare:

CREATE FUNCTION pgedge_vectorizer.count_tokens(p_text TEXT) RETURNS INT
AS 'MODULE_PATHNAME', 'pgedge_vectorizer_count_tokens_sql'
LANGUAGE C IMMUTABLE STRICT;

The PR description calls this "a SQL declaration for the existing C count_tokens() function" — but the existing count_tokens in src/tokenizer.c:32 is an internal C function (int count_tokens(const char *text, const char *model)), not a PG_FUNCTION_INFO_V1 SQL wrapper. The symbol pgedge_vectorizer_count_tokens_sql is defined nowhere — it appears only in these two SQL files. This PR changes no C files.

I verified this by building the branch locally (PG 17.4) and inspecting the compiled library:

$ nm -gU pgedge_vectorizer.dylib | grep -i count_tokens
(no matches)

$ nm -gU pgedge_vectorizer.dylib | grep -i bm25_tokenize   # a real SQL fn, for contrast
_pg_finfo_pgedge_vectorizer_bm25_tokenize
_pgedge_vectorizer_bm25_tokenize

Consequences

  • Any call to pgedge_vectorizer.count_tokens(...) fails at runtime with ERROR: could not find function "pgedge_vectorizer_count_tokens_sql" in file ".../pgedge_vectorizer.so".
  • Worse: the PR rewrites all three chunk-write paths (recreate_chunks() and the trigger/existing-row insert paths — lines 358, 675, 970 in --1.1.sql) to call pgedge_vectorizer.count_tokens(chunk_text). So after upgrading to 1.1, every chunking operation errors outenable_vectorization(), the insert trigger, and recreate_chunks() all break. This is a severe regression, not just a broken new helper.
  • refresh_token_counts() is dead on arrival for the same reason.

Suggested fix

Add the SQL-callable wrapper in src/tokenizer.c:

PG_FUNCTION_INFO_V1(pgedge_vectorizer_count_tokens_sql);
Datum
pgedge_vectorizer_count_tokens_sql(PG_FUNCTION_ARGS)
{
    text *txt = PG_GETARG_TEXT_PP(0);
    char *s = text_to_cstring(txt);
    PG_RETURN_INT32(count_tokens(s, pgedge_vectorizer_model));
}

(plus the prototype in src/pgedge_vectorizer.h). Then regenerate test/expected/count_tokens.out from an actual run — the committed .out cannot have come from a passing installcheck, since the symbol does not resolve.


🟡 Should Fix — get the fork CI run approved

The installcheck regression suite never executed for this PR (fork workflow pending approval). Once the C fix is in, please have a maintainer approve the workflow run so the new count_tokens regression test actually runs before merge. The absent regression run should be treated as a blocker in its own right.


ℹ️ Notes (non-blocking)

  • Semantics change in the chunk-write paths. The old inline length(chunk_text) / 4 used byte length with floor division; count_tokens() uses UTF-8 character count with ceiling division (n+3)/4. This is an improvement (it makes the plpgsql paths consistent with the C chunker, which already uses count_tokens) and is correctly reflected in the tests ('你好世界' → 1). Worth a changelog line since stored token_count values will change for multibyte/short text.
  • refresh_token_counts() evaluates count_tokens(content) twice (in SET and the IS DISTINCT FROM guard). Minor; acceptable since the function is IMMUTABLE and the no-op-skip guard is a nice addition.
  • format('UPDATE %s ...', p_chunk_table) is injection-safe because p_chunk_table is regclass (output is a validated, properly-quoted, schema-qualified identifier). Consistent with existing code. 👍

Once the C wrapper lands and the regression suite passes for real, this is a nice addition. Thanks @syedkazim110!

syedkazim110 and others added 4 commits June 8, 2026 15:43
Add a SQL declaration for the existing C count_tokens() function so
users can call pgedge_vectorizer.count_tokens(text) directly to inspect
approximate token counts (~4 chars/token, UTF-8 aware).

Add refresh_token_counts(chunk_table regclass) to back-fill the
token_count column on existing chunk rows without a full
recreate_chunks() rebuild. Returns the number of rows updated.

Includes regression tests covering approximation values, NULL/UTF-8
handling, the refresh path, and schema-qualified chunk tables.
The three PLpgSQL insert paths (enable_vectorization, vectorization_trigger,
recreate_chunks) were using length(chunk_text) / 4 (floor division) while
count_tokens() and refresh_token_counts() use ceiling division
((char_count + 3) / 4). This meant refresh_token_counts() would silently
change values the trigger had just written for any chunk whose length is
not divisible by 4.

Replace all six occurrences (three per SQL file) with
pgedge_vectorizer.count_tokens(chunk_text) so every write path uses the
same formula.
…ions

Add WHERE token_count IS DISTINCT FROM count_tokens(content) so the
UPDATE only touches rows whose stored value actually differs. On large
chunk tables this avoids unnecessary WAL writes and dead tuples.
The return value now reflects rows that changed, not rows scanned.

Tighten the two refresh test assertions from >= 1 to = COUNT(*) so a
partial update cannot silently pass the regression test.
The SQL migration files declared pgedge_vectorizer_count_tokens_sql as
a C symbol but the wrapper was never added to src/tokenizer.c, causing
every call to pgedge_vectorizer.count_tokens() (and all three chunk-write
paths that call it) to fail with "could not find function" at runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@syedkazim110 syedkazim110 force-pushed the feature/count-tokens-expose branch from 0dd7f16 to 74f9073 Compare June 8, 2026 10:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants