Header logo.
small hallucinations
homeyearstagsaboutrss

“Asia/Beijing” doesn’t exist

I was writing a Python script last week and the ORM threw a tantrum, asking for an “offset-aware” datetime.

ChatGPT (GPT-4o) suggested I use the zoneinfo package and simply specify the name of the timezone.

Naturally, I assumed the timezone name for Beijing would be “Asia/Beijing” and asked GPT-4o for confirmation.

What is the timezone name for Beijing: Asia/Beijing

To be sure, I asked again. (I missed a question mark in my first question, which might have been caused an issue.)

But this didn't work.

Because the timezone for most part of China (+08:00) is actually called “Asia/Shanghai”. Shanghai is the most populous city in mainland China after all.

When I asked again the next day, GPT-4o gave me the correct answer: “Asia/Shanghai”.

So, either hallucinations are random, or the first time ChatGPT thought I was asserting rather than asking.

Recent readings

How to Work with SQL Databases in Go

This post is a good point of reference that clarifies how you can use transactions and contexts while working with SQL databases in Go. The author of this post, Jia Hao, also shares his learnings about algorithms and Elixir on his website.

Web Development with Go

This course is pretty good. Go has a rather straightforward syntax. When you want to build something using Go, there are a lot of options to choose from at every step. Which framework should you use? Do you need an ORM? How shoud you manage schema changes and run migrations? In addition, the internet will always tell you to “just use the standard library” because it already does everything well.

In this course, the author will teach you his preferred way to do things. What's equally, if not more, valuable is he explains background knowledge, technicalities, compromises that he mas made while making technical choices.

If you find the price tag intimidating, scroll down and you'll find a PDF version that is sold at a lower price.

Fixing addon bugs for Anki 23

I've been using Anki for years. Anki has recently changed its version numbers to year.month. From version 23.10, FSRS algorithm is implemented for scheduling reviews. Without thinking too much, I upgraded to the latest version.

Not surprisingly, this upgrade broke a few addons on my computer:

  1. Syntax Highlighting for Code
  2. Mini Format Pack
  3. Zoom 2.1; and
  4. Customize Keyboard Shortcuts

Fixing them was easier than I had thought.


When you start Anki, you are greeted with this alert if there's a bug in an addon.

By clicking “copy debug info”, you'll have some clue around how to move forward.

This is the first clue for fixing Syntax Highlighting for Code:

Anki 23.12.1 (1a1d4d54)  (ao)
Python 3.9.15 Qt 6.5.3 PyQt 6.5.3
Platform: macOS-13.0.1-arm64-arm-64bit
When loading Syntax Highlighting for Code:
Traceback (most recent call last):
# ...
  ".../Application Support/Anki2/addons21/1463041493/consts.py", line 22, in 
    addon_path = os.path.dirname(__file__).decode(sys_encoding)
AttributeError: 'str' object has no attribute 'decode'

By following this path, we can fix this bug by simply changing line 22 into something like this:

1anki21 = version.startswith("2.1.") or version.startswith("23")

The same change fixed Mini Format Pack too.


The reason why Zoom 2.1 stopped working is it imports Qt5 when Anki is using Qt6 now.

Long story short, go to the addon folder and change the import section in __init__.py to this:

1from PyQt6.QtCore import Qt
2from PyQt6.QtGui import QKeySequence, QAction
3from PyQt6.QtWidgets import QMenu

While debugging, you'll need to check the actual value of variables. The way I did it is: dump a value to JSON, create a new error that includes this JSON and raise this error.

The downside is you have to quit Anki and start it again over and over. Luckily it didn't take me too much time to fix the first three addons.

I only scratched the surface of Customize Keyboard Shortcuts as the logic inside it is a bit complicated. If you want to fix it, here's something you can start with:

The constant Qt.Key_Enter is no longer valid. You'll need to use Qt.Key.Key_Enter.

Something clever in the syntax of Elixir

I find this quite clever in the syntax of Elixir.

There are two sets of boolean operators in Elixir and, or, not versus &&, ||, !.

The difference is that and, or, not only take literal boolean values. In contrast, &&, ||, ! can take values of all types. And all values except false and nil will evaluate to true.

When you think about it, it makes a lot of sense. Because literal boolean values -- true and false -- are words. It's only natural that they go with words: and, or, not.

Adding a new context and define associations in Phoenix

I'm still doing Dockyard's tutorial of Elixir and Phoenix.

So far I've progressed to building a blog context. Now I'm trying to add a comment context to the app. This would also allow the user to post a comment and list all the comments below a post.

This exercise is designed to teach you about how to create one-to-many relations in Phoenix.

Although the concept is easy to grasp, there are a few steps you need to take and quite a few files you need to change.

The rest of this post shows how I did it. The idea is to remind myself and show other beginners of Phoenix how to do it on a nitty-gritty level. I can't guarantee this is best practice.

SPOILER ALERT If you are also doing this exercise, I strongly recommend you to work on it on your own first.


Steps

For the sake of clarity, suppose we are adding comments to posts.

  1. generate a new context (comments), edit migration file and migrate
  2. change schemas to specify the association between them
  3. change posts repo file to preload comments
  4. change router file to specify which controller function should handle requests made to comments
  5. change comment controller to handle those requests
  6. change post controller to add comment_changeset
  7. change post template to display and edit comments

1. generate a new context; edit migration file and migrate

Generate a context by doing this:

mix phx.gen.context Comments Comment comments content:text post_id:references:posts

Generate a migration file (I forgot if the last command would generate a migration file. So this may or may not be necessary.):

mix ecto.gen.migration add_comments

In the generated empty migration file, add this. This defines a comments table of two fields. null: false makes sure a comment must point to a post.

1def change do
2  create table(:comments) do
3    add :content, :text
4    add :post_id, references(:posts, on_delete: :nothing), null: false
5
6    timestamps()
7  end

Run the migration file.

2. schemas

In lib/blog/posts/post.ex (this file defines the posts schema). Add:

1defmodule Blog.Posts.Post do
2  # ...
3
4  schema "posts" do
5    # ... other fields ...
6    has_many :comments, Blog.Comments.Comment
7    # ...
8  end
9  # ...

In lib/blog/comments/comment.ex (this file defines the comments schema).

 1defmodule Blog.Comments.Comment do
 2  use Ecto.Schema
 3  import Ecto.Changeset
 4
 5  schema "comments" do
 6    field :content, :string
 7    # ADD belongs_to
 8    belongs_to :post, Blog.Posts.Post
 9
10    timestamps()
11  end
12
13  @doc false
14  def changeset(comment, attrs) do
15    comment
16    # ADD post_id
17    |> cast(attrs, [:content, :post_id])
18    |> validate_required([:content])
19    |> foreign_key_constraint(:post_id)
20  end
21end

post_id is added to the cast function so that the created comments will accept and store a post_id value.

3. posts repo

In lib/blog/posts.ex add preload to preload comments. This file defines the Blog.Posts module along with functions to get, create and change posts.

This way, comments associated with the post record will be preloaded. So that the HEEx template will have something to display.

1def get_post!(id) do
2  Repo.get!(Post, id)
3  |> Repo.preload(:comments) # ADDED
4end

4. router

In lib/blog_web/router.ex (where the router is defined), add a route to handle the post request made to /comments. This request is used to create a comment.

1scope "/", BlogWeb do
2  pipe_through :browser
3
4  get "/", PageController, :home
5  post "/comments", CommentController, :create # ADDED
6  resources "/posts", PostController
7end

5. comment controller

At lib/blog_web/controllers/comment_controller.ex create a comment controller. This is actually directly taken from Dockyard tutorial.

 1defmodule BlogWeb.CommentController do
 2  use BlogWeb, :controller
 3
 4  alias Blog.Posts
 5  alias Blog.Comments
 6
 7  def create(conn, %{"comment" => comment_params}) do
 8    case Comments.create_comment(comment_params) do
 9      {:ok, comment} ->
10        conn
11        |> put_flash(:info, "Comment created successfully.")
12        |> redirect(to: ~p"/posts/#{comment.post_id}")
13
14      {:error, %Ecto.Changeset{} = comment_changeset} ->
15        post = Posts.get_post!(comment_params["post_id"])
16        render(conn, :show, post: post, comment_changeset: comment_changeset)
17    end
18  end
19end

6. post controller

In lib/blog_web_controllers/post_controller.ex, because we are trying to show comments below each blog post, we need to add a comment_changeset to the show function.

1def show(conn, %{"id" => id}) do
2  post = Posts.get_post!(id)
3  comment_changeset = Comments.change_comment(%Comment{})
4  render(conn, :show,
5    post: post,
6    comment_changeset: comment_changeset
7    )
8end

7. template

In lib/blog_web/controllers/post_html/show.html.heex, we need to add these lines to be able to create and display the comments. (A lot of the code in this part is also taken from the tutorial.)

 1<.simple_form :let={f} for={@comment_changeset} action={~p"/comments"}>
 2  <.input type="hidden" field={f[:post_id]} value={@post.id} />
 3  <.input field={f[:content]} type="text" label="Content" />
 4  <:actions>
 5    <.button>Comment</.button>
 6  </:actions>
 7</.simple_form>
 8
 9<%= for comment <- @post.comments do %>
10  <p><%= comment.content %></p>
11<% end %>

Ecto search keyword interpolation

Updated on 2023-11-27:

Now I realize I didn't need to use a fragment. I could have used ilike.

The difference between ilike and like is that ilike is explicitly case-insensitive, whereas like could be case sensitive based on the database you are using.


Original post:

I was doing Dockyard's tutorial of Elixir and Phoenix. It took me an hour to figure out how to search a field for partial matching strings.

tl;dr, here's the code:

 1def search_posts(keyword) do
 2  search_pattern = "%#{keyword}%" # ❤️
 3  Repo.all(
 4    from p in Post,
 5    where:
 6      fragment("? LIKE ?",
 7          p.title,
 8          ^search_pattern)
 9  )
10end

Inside an Ecto.Query statement, we need to use the caret to interpolate a variable. And this was novel to me. (Macros in Elixir enable innovative syntax, which is certainly powerful, but tends to be elusive to beginners.)

If we wrote SQL, it would be something like this, % being the wildcard for any number of chars.

1SELECT * WHERE 'title' LIKE '%word%'

In order to send a query that looks something like the above, I used a fragment:

1def search_posts(keyword) do
2  Repo.all(
3    from p in Post,
4    where: fragment("? LIKE %?%", p.title, ^keyword)
5  )
6end

For some reason the SQL query after interpolation ended up looking like this.

1SELECT p0."id", p0."title", p0."subtitle",
2p0."content", p0."inserted_at",
3p0."updated_at"
4FROM "posts" AS p0
5WHERE (p0."title" LIKE %$1%)

That's why I defensively used interpolation to get search_pattern on line ❤️ .

Untracking files in Git

Normally you can avoid tracking files in Git by adding files to .gitignore.

But if files have been previously added for tracking, they will still be tracked even after appending them to .gitignore.

We need to first remove tracked files from cache. And add back all the files – including .gitignore – in the current directory. Now .gitignore will take effect.

1git rm -r --cached .
2git add .