{"id":20582815,"url":"https://github.com/malted/activerecord-tracer","last_synced_at":"2026-03-06T20:31:48.484Z","repository":{"id":237962484,"uuid":"795587176","full_name":"malted/activerecord-tracer","owner":"malted","description":"A ray tracing renderer written in Rails' ActiveRecord","archived":false,"fork":false,"pushed_at":"2024-05-11T18:07:56.000Z","size":5351,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-29T19:59:53.928Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/malted.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-05-03T15:46:07.000Z","updated_at":"2024-08-22T19:26:52.000Z","dependencies_parsed_at":"2024-05-11T19:24:26.691Z","dependency_job_id":"3c9ba052-440c-4d54-a046-1a873d4b3add","html_url":"https://github.com/malted/activerecord-tracer","commit_stats":null,"previous_names":["malted/activerecord-tracer"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/malted/activerecord-tracer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/malted%2Factiverecord-tracer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/malted%2Factiverecord-tracer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/malted%2Factiverecord-tracer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/malted%2Factiverecord-tracer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/malted","download_url":"https://codeload.github.com/malted/activerecord-tracer/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/malted%2Factiverecord-tracer/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30196173,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-06T19:07:06.838Z","status":"ssl_error","status_checked_at":"2026-03-06T18:57:34.882Z","response_time":250,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-16T06:37:13.288Z","updated_at":"2026-03-06T20:31:48.457Z","avatar_url":"https://github.com/malted.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003e [!NOTE]  \n\u003e I gave a talk about this at RailsConf 2024. Below is a small excerpt.\n\n# Raytraced rendering in ActiveRecord\n![Render output](res/out.png)\n\n###### The output of `render.rb`, rendered in about half a second.\n\n## Explanation\n\nIn order to generate so many pixel values, we're going to use a quite uncommon SQL construct called a recursive common table expression.\nA common table expression (CTE) is created with the `WITH` keyword, and is used for creating temporary named result sets. This lets you break down big queries into smaller chunks, giving you intermediary tables in a virtual table, so you don't have to `CREATE TABLE`, calculate stuff, then `DROP TABLE`. `WITH RECURSIVE` does this too, but uses its own table data again and again until there are no more rows (ie some condition is met).\n\nSay we want to construct an SQL query that simply counts up to 10.\n\nFirst, let's define a recursive CTE with `WITH RECURSIVE`. Then, name it (just \"foo\" in this example). I'm also going to explicitly define a column named `n`, which will be the only one in the CTE and will contain our number.\n```sql\nWITH RECURSIVE foo(n)\n```\nthen, we need to create the create the actual recursive query.\n```sql\nSELECT 1     -- this will be the inital value of n.\nUNION ALL    -- combining the result of SELECT 1 with the following SELECT resultset\nSELECT n + 1 -- the recursive bit - takes the value of n from the previous step and increments it\nFROM foo     -- referring to our CTE\nWHERE n \u003c 10 -- we don't want to go on forever! This is our end condition\n```\nput it together and we get\n```sql\nWITH RECURSIVE foo(n) AS (SELECT 1 UNION ALL SELECT n + 1 FROM foo LIMIT 10) SELECT n FROM foo;\n```\n\n```sql\nsqlite\u003e WITH RECURSIVE foo(n) AS (SELECT 1 UNION ALL SELECT n + 1 FROM foo LIMIT 10) SELECT n FROM foo;\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n```\nThis looks like what we want, but there's a problem; each number is its own row. If we were querying real data, this would be exactly what we want, but we're trying to construct a string here.\nWhen we run\n```ruby\nActiveRecord::Base.connection.execute(\"WITH RECURSIVE foo(n) AS (SELECT 1 UNION ALL SELECT n + 1 FROM foo WHERE n \u003c 10) SELECT n FROM foo\")\n```\nwe get the following back:\n```ruby\n{\"n\"=\u003e1}\n{\"n\"=\u003e2}\n{\"n\"=\u003e3}\n{\"n\"=\u003e4}\n{\"n\"=\u003e5}\n{\"n\"=\u003e6}\n{\"n\"=\u003e7}\n{\"n\"=\u003e8}\n{\"n\"=\u003e9}\n{\"n\"=\u003e10}\n```\nNow, we *could* just concat them in Ruby, like so\n```ruby\nres = ActiveRecord::Base.connection.execute(\"WITH RECURSIVE foo(n) AS (SELECT 1 UNION ALL SELECT n + 1 FROM foo WHERE n \u003c 10) SELECT n FROM foo\")\nputs res.map { |curr| curr[\"n\"] }.join(\", \") # 1, 2, 3, 4, 5, 6, 7, 8, 9, 10\n```\nBut the goal here is to do as much computation in SQL as possible. Luckily, `SQLite` has both a print function (a pseudo-reimplementation of C's `stdio.h` `printf`) and a row concatenation function - [`PRINTF`](https://sqlite.org/printf.html) and [`GROUP_CONCAT`](https://www.sqlite.org/lang_aggfunc.html#group_concat) respectively.\nWe can use them to format our rows however we like.\n```sql\nsqlite\u003e WITH RECURSIVE foo(n) AS (SELECT 1 UNION ALL SELECT n + 1 FROM foo LIMIT 10) SELECT GROUP_CONCAT(PRINTF(\"number: %i\", n), ', ') FROM foo;\nnumber: 1, number: 2, number: 3, number: 4, number: 5, number: 6, number: 7, number: 8, number: 9, number: 10\n```\nHere, `PRINTF(\"number: %i\", n)` is formatting each row (we would write `\"number #{n}\"` in Ruby) and `GROUP_CONCAT` is essentially acting as Ruby's `Array#join`.\n\n### Where `Arel::Nodes::NamedFunction` breaks down\nAFAIK, Arel does not support defining common table expressions with explicit columns.\nI thought I could define it as a `NamedFunction` node, like so;\n\n```ruby\nArel::Nodes::NamedFunction.new(\"numbers\", [Arel.sql(\"n\")])\n```\nbut as I understand this is for *calling* named functions, not defining them, because while the direct `.to_sql` output *is* what's needed for defining a CTE, when used in more complex queries it breaks down, as demonsrated below.\n\nThe following works as expected;\n```ruby\ncte_def = Arel::Nodes::NamedFunction.new(\"numbers\", [Arel.sql(\"n\")])\nas_stmt = Arel::Nodes::As.new cte_def, Arel.sql(\"foo\")\nputs as_stmt.to_sql\n```\nOutputs `numbers(n) AS foo`.\n\nBut when used within another method, not so much\n\n```ruby\ncte_def = Arel::Nodes::NamedFunction.new(\"numbers\", [Arel.sql(\"n\")])\nas_stmt = Arel::Nodes::As.new cte_def, Arel.sql(\"foo\")\nselect_manager = Arel::SelectManager.new\nputs select_manager.with(as_stmt).to_sql\n```\nOutputs `WITH \"numbers\" AS foo SELECT`\nIt turns out that the `Node::NamedFunction`'s `name` method is being called, which we defined as `numbers`. Secondly, because the return type of `name` is a string, Arel is wrapping it in quotes in the sql output.\n\nFor now, I'm going to make a very hacky fix, but I hope it can be fixed in idiomatic Arel. If you know how, let me know.\n\n```ruby\ncte_def = Arel::Nodes::NamedFunction.new(\"numbers\", [Arel.sql(\"n\")])\ncte_def.define_singleton_method(:name) do\n    # Making this an SqlLiteral node ensures it's not wrapped in quotes, as a string would be.\n    Arel.sql(\"numbers(n)\")\nend\n```\n\n### Sources\n* https://stackoverflow.com/q/57613637/10652680\n* https://jpospisil.com/2014/06/16/the-definitive-guide-to-arel-the-sql-manager-for-ruby\n* https://rubydoc.info/gems/arel/\n* The ActiveRecord Github repo, especially the tests!\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmalted%2Factiverecord-tracer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmalted%2Factiverecord-tracer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmalted%2Factiverecord-tracer/lists"}