The usual seekr workflow starts from files.
You call seek(), seekr(), or
match_files() to search files and create a
seekr_match vector. You inspect, filter, or update that
vector. Then, if needed, you pass it to replace_files() to
apply the selected replacements back to disk.
That workflow is useful when files are the source of truth and when
you want seekr to handle file reading and writing for
you.
But sometimes the text you want to search does not come directly from files, or you need more control over how text is read, transformed, and written. For example, text may come from a database, an API, a package object, a clipboard, a web request, or a custom file-reading function. You may also want to decide exactly what happens after replacement: update a database row, send the result to an API, write a custom SQL script, preserve a specific encoding, or use your own output format.
For those cases, seekr provides two lower-level
counterparts to match_files() and
replace_files():
-
match_text()searches one in-memory string and returns aseekr_matchvector. -
replace_text()applies aseekr_matchvector back to an in-memory string and returns the modified text.
These functions let you use seekr as a
search-and-replacement engine for strings. You still get structured
match objects, context printing, replacement previews, filtering, and
safety checks, but you remain responsible for where the text comes from
and where the modified text goes.
Example: updating SQL procedures from a table
Imagine that you have extracted a few SQL procedures from a database.
In this example, the procedures are already available in a data frame. In a real workflow, this table could have been created from a database query, an API call, or any other source.
df
#> # A tibble: 5 × 2
#> name body
#> <chr> <chr>
#> 1 refresh_customer_scores.SQL "\nCREATE OR REPLACE FUNCTION refresh_customer_scores()\nRETURNS void AS $$\nBEGIN\n UPDATE customer_scores\n SET ol…
#> 2 refresh_offer_flags.SQL "\nCREATE OR REPLACE FUNCTION refresh_offer_flags()\nRETURNS void AS $$\nBEGIN\n UPDATE offer_flags\n SET old_flag =…
#> 3 archive_old_events.SQL "\nCREATE OR REPLACE FUNCTION archive_old_events()\nRETURNS void AS $$\nBEGIN\n DELETE FROM events\n WHERE event_dat…
#> 4 refresh_campaign_targets.SQL "\nCREATE OR REPLACE FUNCTION refresh_campaign_targets()\nRETURNS void AS $$\nBEGIN\n UPDATE campaign_targets\n SET …
#> 5 cleanup_temp_tables.SQL "\nCREATE OR REPLACE FUNCTION cleanup_temp_tables()\nRETURNS void AS $$\nBEGIN\n DELETE FROM temp_work_table;\nEND;\n…Suppose we want to rename some old SQL identifiers before reviewing and re-running the procedures.
The text is not coming from files, so we use
match_text() instead of match_files().
match_text() searches one string at a time. It also
needs a path, which is used as a source identifier in the
resulting seekr_match vector. Here, we use the procedure
name as that identifier.
df <-
df |>
mutate(
x = map2(
name,
body,
\(name, body) match_text(
text = body,
path = name,
pattern = "old_([a-z_]+)",
replacement = "new_\\1"
)
),
.after = name
)The result is a list-column of seekr_match vectors.
df
#> # A tibble: 5 × 3
#> name x body
#> <chr> <list> <chr>
#> 1 refresh_customer_scores.SQL <seekr::match [2]> "\nCREATE OR REPLACE FUNCTION refresh_customer_scores()\nRETURNS void AS $$\nBEGIN\n UPDATE custom…
#> 2 refresh_offer_flags.SQL <seekr::match [1]> "\nCREATE OR REPLACE FUNCTION refresh_offer_flags()\nRETURNS void AS $$\nBEGIN\n UPDATE offer_flag…
#> 3 archive_old_events.SQL <seekr::match [1]> "\nCREATE OR REPLACE FUNCTION archive_old_events()\nRETURNS void AS $$\nBEGIN\n DELETE FROM events…
#> 4 refresh_campaign_targets.SQL <seekr::match [1]> "\nCREATE OR REPLACE FUNCTION refresh_campaign_targets()\nRETURNS void AS $$\nBEGIN\n UPDATE campa…
#> 5 cleanup_temp_tables.SQL <seekr::match [0]> "\nCREATE OR REPLACE FUNCTION cleanup_temp_tables()\nRETURNS void AS $$\nBEGIN\n DELETE FROM temp_…Each row still contains the original text and the matches found in that text. Here, we keep only procedures where at least one match was found.
df <-
df |>
filter(!map_lgl(x, is_empty))
df
#> # A tibble: 4 × 3
#> name x body
#> <chr> <list> <chr>
#> 1 refresh_customer_scores.SQL <seekr::match [2]> "\nCREATE OR REPLACE FUNCTION refresh_customer_scores()\nRETURNS void AS $$\nBEGIN\n UPDATE custom…
#> 2 refresh_offer_flags.SQL <seekr::match [1]> "\nCREATE OR REPLACE FUNCTION refresh_offer_flags()\nRETURNS void AS $$\nBEGIN\n UPDATE offer_flag…
#> 3 archive_old_events.SQL <seekr::match [1]> "\nCREATE OR REPLACE FUNCTION archive_old_events()\nRETURNS void AS $$\nBEGIN\n DELETE FROM events…
#> 4 refresh_campaign_targets.SQL <seekr::match [1]> "\nCREATE OR REPLACE FUNCTION refresh_campaign_targets()\nRETURNS void AS $$\nBEGIN\n UPDATE campa…We can also inspect all matches together by combining the list-column
into one seekr_match vector.
x <- reduce(df$x, c, .init = new_seekr_match())
print(x, context = 2L)
#> <seekr::match[5]> 4 sources
#> refresh_customer_scores.SQL [2]
#> 4 | BEGIN
#> 5 | UPDATE customer_scores
#> [1] -- 6 | SET old_score = old_score + 1;
#> ++ 6 | SET new_score = old_score + 1;
#> [2] -- 6 | SET old_score = old_score + 1;
#> ++ 6 | SET old_score = new_score + 1;
#> 7 | END;
#> 8 | $$ LANGUAGE plpgsql;
#>
#> refresh_offer_flags.SQL [1]
#> 4 | BEGIN
#> 5 | UPDATE offer_flags
#> [3] -- 6 | SET old_flag = TRUE
#> ++ 6 | SET new_flag = TRUE
#> 7 | WHERE active = TRUE;
#> 8 | END;
#>
#> archive_old_events.SQL [1]
#> 1 |
#> [4] -- 2 | CREATE OR REPLACE FUNCTION archive_old_events()
#> ++ 2 | CREATE OR REPLACE FUNCTION archive_new_events()
#> 3 | RETURNS void AS $$
#> 4 | BEGIN
#>
#> refresh_campaign_targets.SQL [1]
#> 4 | BEGIN
#> 5 | UPDATE campaign_targets
#> [5] -- 6 | SET old_segment = 'active'
#> ++ 6 | SET new_segment = 'active'
#> 7 | WHERE last_seen >= CURRENT_DATE - INTERVAL '30 days';
#> 8 | END;At this point, x is a regular seekr_match
vector and can be inspected like any other search result.
summary(x)
#> ── <seekr::match[5]> ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
#> Top sources [4]
#> • refresh_customer_scores.SQL : 2 (40.0%)
#> • archive_old_events.SQL : 1 (20.0%)
#> • refresh_campaign_targets.SQL : 1 (20.0%)
#> • refresh_offer_flags.SQL : 1 (20.0%)
#>
#> Top matches/replacements [4]
#> • <old_score/new_score> : 2 (40.0%)
#> • <old_events/new_events> : 1 (20.0%)
#> • <old_flag/new_flag> : 1 (20.0%)
#> • <old_segment/new_segment> : 1 (20.0%)
#>
#> Top extension [1]
#> • sql : 5 (100.0%)
#>
#> Top encoding [1]
#> • NA : 5 (100.0%)Applying replacements in memory
We can now apply the selected replacements with
replace_text().
Unlike replace_files(), this does not write anything to
disk. It simply returns a modified string, which we store here as
another column.
df <-
df |>
mutate(
replaced = map2_chr(
body,
x,
\(body, x) replace_text(text = body, x = x)
),
.before = body
)
df
#> # A tibble: 4 × 4
#> name x replaced body
#> <chr> <list> <chr> <chr>
#> 1 refresh_customer_scores.SQL <seekr::match [2]> "\nCREATE OR REPLACE FUNCTION refresh_customer_scores()\nRETURNS void AS $$\nBEGIN\n UPDATE … "\nC…
#> 2 refresh_offer_flags.SQL <seekr::match [1]> "\nCREATE OR REPLACE FUNCTION refresh_offer_flags()\nRETURNS void AS $$\nBEGIN\n UPDATE offe… "\nC…
#> 3 archive_old_events.SQL <seekr::match [1]> "\nCREATE OR REPLACE FUNCTION archive_new_events()\nRETURNS void AS $$\nBEGIN\n DELETE FROM … "\nC…
#> 4 refresh_campaign_targets.SQL <seekr::match [1]> "\nCREATE OR REPLACE FUNCTION refresh_campaign_targets()\nRETURNS void AS $$\nBEGIN\n UPDATE… "\nC…This is the point of the in-memory workflow: seekr
helped us find, inspect, filter, and safely apply replacements, but it
did not decide what to do with the result.
Writing a custom SQL script
Because the updated procedures are ordinary strings, we can write them however we want.
For example, we can combine the updated procedures into a single SQL script. We can also add comments, separators, or any other metadata that makes sense for the target system.
sql_script <-
df |>
mutate(
script_block = paste0(
"-- Procedure: ", name, " -------------------------------\n",
replaced
)
) |>
pull(script_block)
cat(sql_script, sep = "\n\n")
#> -- Procedure: refresh_customer_scores.SQL -------------------------------
#>
#> CREATE OR REPLACE FUNCTION refresh_customer_scores()
#> RETURNS void AS $$
#> BEGIN
#> UPDATE customer_scores
#> SET new_score = new_score + 1;
#> END;
#> $$ LANGUAGE plpgsql;
#>
#>
#> -- Procedure: refresh_offer_flags.SQL -------------------------------
#>
#> CREATE OR REPLACE FUNCTION refresh_offer_flags()
#> RETURNS void AS $$
#> BEGIN
#> UPDATE offer_flags
#> SET new_flag = TRUE
#> WHERE active = TRUE;
#> END;
#> $$ LANGUAGE plpgsql;
#>
#>
#> -- Procedure: archive_old_events.SQL -------------------------------
#>
#> CREATE OR REPLACE FUNCTION archive_new_events()
#> RETURNS void AS $$
#> BEGIN
#> DELETE FROM events
#> WHERE event_date < CURRENT_DATE - INTERVAL '365 days';
#> END;
#> $$ LANGUAGE plpgsql;
#>
#>
#> -- Procedure: refresh_campaign_targets.SQL -------------------------------
#>
#> CREATE OR REPLACE FUNCTION refresh_campaign_targets()
#> RETURNS void AS $$
#> BEGIN
#> UPDATE campaign_targets
#> SET new_segment = 'active'
#> WHERE last_seen >= CURRENT_DATE - INTERVAL '30 days';
#> END;
#> $$ LANGUAGE plpgsql;The script could then be written to disk, copied into a SQL client, sent to a deployment tool, or reviewed manually.
Safety checks still apply
replace_text() follows the same safety principle as
replace_files().
A seekr_match vector records a hash of the text that was
searched. When you call replace_text(), seekr
checks that the text supplied to replace_text() is the same
text that was used in match_text().
The safe workflow is the same as with files: if the text changed, search again.
Why not use stringr directly?
For straightforward replacements, stringr is often the
right choice. If you know exactly what to replace and want to replace
every occurrence, stringr::str_replace_all() is simpler
than creating a seekr_match vector.
seekr becomes useful when you want to inspect matches
before acting on them: print them with context, preview replacements,
filter out false positives, update replacements match by match, and then
apply only the selected changes. With direct replacement, search and
replacement happen at the same time, which leaves no room for that kind
of review.
match_text() and replace_text() give you
that inspectable workflow for text already in memory, without reading
from or writing to files.
