[{"content":"","date":"12 July 2026","externalUrl":null,"permalink":"/en/tags/database/","section":"Tags","summary":"","title":"Database","type":"tags"},{"content":"","date":"12 July 2026","externalUrl":null,"permalink":"/en/tags/go/","section":"Tags","summary":"","title":"Go","type":"tags"},{"content":"","date":"12 July 2026","externalUrl":null,"permalink":"/en/tags/gorm/","section":"Tags","summary":"","title":"Gorm","type":"tags"},{"content":"","date":"12 July 2026","externalUrl":null,"permalink":"/en/tags/postgresql/","section":"Tags","summary":"","title":"Postgresql","type":"tags"},{"content":"","date":"12 July 2026","externalUrl":null,"permalink":"/en/posts/","section":"Posts","summary":"","title":"Posts","type":"posts"},{"content":"","date":"12 July 2026","externalUrl":null,"permalink":"/en/tags/sqlc/","section":"Tags","summary":"","title":"Sqlc","type":"tags"},{"content":"","date":"12 July 2026","externalUrl":null,"permalink":"/en/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"12 July 2026","externalUrl":null,"permalink":"/en/","section":"Thoughts","summary":"","title":"Thoughts","type":"page"},{"content":"မြန်မာဘာသာဖြင့် ဖတ်ရှုရန်\nIntroduction # Recently, I have been building image-server, an image upload and transformation server in Go. For the database layer, I had to choose between two popular options: an ORM like GORM, or a code generator like sqlc.\nI chose sqlc. In this post, I will share what sqlc is, how it works, its pros and cons, a quick comparison with GORM, and how I configured it in my project, including the git hooks that keep the generated code safe.\nWhat is sqlc? # sqlc is not an ORM. You write plain SQL in .sql files, and sqlc generates type-safe Go code from them. There is no query builder and no magic at runtime. Everything happens at build time, before your code even compiles.\nFor example, this is a query file in my project:\n-- name: GetImage :one SELECT * FROM images WHERE id = $1; -- name: CreateImage :one INSERT INTO images ( original_filename, content_hash, mime_type, width, height, size_bytes, storage_key ) VALUES ( $1, $2, $3, $4, $5, $6, $7 ) RETURNING *; After running sqlc generate in terminal, I get a Go function like this:\nfunc (q *Queries) GetImage(ctx context.Context, id uuid.UUID) (Image, error) The Image struct is also generated automatically, with the correct Go types for every column (uuid.UUID, int32, int64, and so on). I don\u0026rsquo;t need to write this code by hand.\nWhere Does sqlc Get the Table Structure? # sqlc does not connect to a live database. In sqlc.yaml, I point it at migration files:\nversion: \u0026#34;2\u0026#34; sql: - engine: \u0026#34;postgresql\u0026#34; queries: \u0026#34;internal/db/queries\u0026#34; schema: \u0026#34;migrations\u0026#34; gen: go: package: \u0026#34;db\u0026#34; out: \u0026#34;internal/db\u0026#34; sql_package: \u0026#34;pgx/v5\u0026#34; sqlc reads every migration in the migrations/ folder, replays them in order like a fresh database, and builds the schema in memory. It is smart enough to skip .down.sql rollback files and only apply the .up.sql files. Then it type-checks every query against that schema.\nSo if I rename a column in a migration but forget to update a query, code generation fails. I find out at build time, not in production.\nPros of sqlc # You write real SQL. No new DSL to learn. Compile-time safety. Wrong types, wrong column names, and wrong parameter counts are all caught before the code runs. No hidden queries. The SQL that hits the database is exactly the SQL you wrote. No surprise N+1 queries. No runtime overhead. The generated code is just QueryRow and Scan. No reflection. SQL injection safe by construction.\u0026quot; Every query is parameterized ($1, $2), so string concatenation never happens. Cons of sqlc # You must write SQL. If your team wants to avoid SQL completely, this is a wall, not a feature. Dynamic queries are awkward. Queries are static strings, so building a search endpoint with five optional filters is painful compared to chaining .Where() calls. No relations or eager loading. If you want joined data, you write the JOIN yourself. Extra build step. Every query change needs a re-generate, and the output must be committed. (Git hooks can solve this, see below.) Migrations are your job. sqlc does not manage schema changes like AutoMigrate() function in GORM; you need a separate migration tool. Quick Comparison with GORM # sqlc GORM What you write Plain SQL Go structs + method chains Type safety Compile time Runtime (reflection) Query visibility Exactly what you wrote Built for you behind the scenes Performance No reflection, no overhead Reflection + possible N+1 queries Dynamic queries Clunky Easy Learning curve Know SQL, tiny Go API Learn GORM\u0026rsquo;s conventions Migrations Separate tool AutoMigrate (risky for production) Neither one is \u0026ldquo;better\u0026rdquo; in every case. GORM shines when you have many entities with relations and lots of dynamic filtering. sqlc shines when you want full control, predictable performance, and queries you can read.\nWhy I Chose sqlc for My Image Server # image-server has one security rule that is non-negotiable: parameterized queries only. There is simply no way to build a query by string concatenation, so SQL injection is not something I can introduce by accident.\nOn top of that, my queries are simple CRUD: create an image record, get it by ID, look it up by content hash for deduplication, and delete it. There is no dynamic filtering at all, so sqlc\u0026rsquo;s biggest weakness never comes up, and its biggest strength (exact SQL with compile-time checking) is all upside.\nKeeping Generated Code in Sync with Git Hooks # Generated code has one classic failure mode: someone edits a query, forgets to re-generate, and commits stale code. I close that gap with git hooks, committed in .githooks/ and activated once per clone:\ngit config core.hooksPath .githooks 1. pre-commit: drift check # The pre-commit hook re-runs the generator and fails the commit if the output differs from what is staged:\nif [[ -f sqlc.yaml ]]; then make -s sqlc-gen \u0026gt; /dev/null if ! git diff --exit-code -- internal/db \u0026gt; /dev/null 2\u0026gt;\u0026amp;1; then echo \u0026#34;❌ internal/db is out of sync with the SQL queries.\u0026#34; echo \u0026#34; Run \u0026#39;make sqlc-gen\u0026#39;, stage the result, and recommit.\u0026#34; exit 1 fi fi So it is impossible to commit a query change without its generated code. The same hook does the identical check for my OpenAPI-generated code.\n2. pre-push: quality gates # The pre-push hook runs the heavier gates before anything leaves my machine: golangci-lint, a 90% test-coverage gate, API tests that validate every endpoint against the OpenAPI spec, and full-stack e2e tests against the real Docker container. CI re-runs the first three gates repo-side, so even --no-verify cannot sneak bad code onto the main branch.\nThe result: schema, queries, and Go types can never silently drift apart.\nConclusion # If your project has complex relations and lots of dynamic queries, GORM is still a reasonable choice. But if you are comfortable with SQL and you value compile-time safety, predictable performance, and knowing exactly what runs against your database, I highly recommend giving sqlc a try. For my image-server project, it has been the right call, and with the git hooks in place, the generated code takes care of itself.\n","date":"12 July 2026","externalUrl":null,"permalink":"/en/posts/sqlc-go/","section":"Posts","summary":"","title":"Why I Use sqlc Instead of GORM in My Image Server Project with Go","type":"posts"},{"content":"","date":"24 March 2026","externalUrl":null,"permalink":"/en/tags/claude/","section":"Tags","summary":"","title":"Claude","type":"tags"},{"content":"","date":"24 March 2026","externalUrl":null,"permalink":"/en/tags/claude-code/","section":"Tags","summary":"","title":"Claude Code","type":"tags"},{"content":"","date":"24 March 2026","externalUrl":null,"permalink":"/en/tags/claude-replay/","section":"Tags","summary":"","title":"Claude-Replay","type":"tags"},{"content":"","date":"24 March 2026","externalUrl":null,"permalink":"/en/tags/tools/","section":"Tags","summary":"","title":"Tools","type":"tags"},{"content":"မြန်မာဘာသာဖြင့် ဖတ်ရှုရန်\nIntroduction # Recently, I have been using both Claude Code and Antigravity for work and personal projects. However, I encountered a problem: it\u0026rsquo;s quite inconvenient to share the conversations from a session with others. Sending long logs or recording a screen capture isn\u0026rsquo;t very practical.\nWhile looking for a solution, I found a tool called claude-replay. claude-replay generates an interactive playback HTML file that allows you to watch AI chat sessions just like a video.\nCurrently, it only supports Claude Code, Cursor, and Codex CLI.\nUseful Commands # Some useful commands you should know:\nclaude-replay: Launch the web editor to browse and view your current sessions. claude-replay abc123def456 -o replay.html: Pass a session ID to extract it and generate an Interactive playback HTML file. claude-replay session.jsonl --no-thinking --no-tool-calls -o replay.html: Hide AI thinking steps and tool calls if you do not want to include them. claude-replay session.jsonl --turns 5-15 --speed 2.0 -o replay.html: Play back only a specific part of the session and customize the playback speed. claude-replay session.jsonl --theme dracula -o replay.html: Change the playback UI color theme easily. Conclusion # For features like Secret redaction and other extremely useful capabilities, as well as the ability to embed the single HTML file into another page, I highly recommend checking out the claude-replay GitHub Repo and trying it out yourself!\n","date":"24 March 2026","externalUrl":null,"permalink":"/en/posts/claude-replay-for-claude-code/","section":"Posts","summary":"","title":"Why claude-replay is useful for Claude Code Users","type":"posts"},{"content":"","date":"12 March 2026","externalUrl":null,"permalink":"/en/tags/git/","section":"Tags","summary":"","title":"Git","type":"tags"},{"content":"","date":"12 March 2026","externalUrl":null,"permalink":"/en/tags/git-reset/","section":"Tags","summary":"","title":"Git Reset","type":"tags"},{"content":"","date":"12 March 2026","externalUrl":null,"permalink":"/en/tags/github/","section":"Tags","summary":"","title":"Github","type":"tags"},{"content":"မြန်မာဘာသာဖြင့် ဖတ်ရှုရန်\nIntroduction # When using Git, you have probably run into a situation where you forgot to include a file in a commit, or made a typo in your commit message. In those cases, git reset --soft lets you undo that commit and fix things up — without losing any of your work.\nIn this post, I\u0026rsquo;ll share how to use git reset --soft HEAD^ and git reset --soft HEAD~n to edit your most recent commit and older commits.\nEditing the Last Commit (git reset \u0026ndash;soft HEAD^) # If you want to undo your last commit and put your files back into the staging area, run this command:\ngit reset --soft HEAD^ This moves HEAD back one step to the parent commit. Because we use --soft, your files are not deleted, they stay in the staging area (index), ready to be re-committed. From there, you can add more files, remove files, or adjust anything you need before making a fresh commit.\nEditing Older Commits (git reset \u0026ndash;soft HEAD~n) # If you want to undo not just the last commit but the last 2 or 3, you can use HEAD~n.\nFor example, to undo the last 3 commits and squash them all into one:\ngit reset --soft HEAD~3 This will undo those 3 commits and place all the changed files back into the staging area, letting you re-commit them as a single, clean commit.\nOther Useful Tips # 1. Difference Between --soft and --mixed # If you use git reset HEAD^ or git reset --mixed HEAD^ (without --soft), your files will be moved out of the staging area into an unstaged (modified) state. You will need to run git add again before you can commit.\n2. Be Careful with git reset --hard! # Using git reset --hard HEAD^ does not just undo the commit — it also permanently deletes all the file changes in that commit. If you only want to undo the commit while keeping your code, make sure to use --soft.\n3. Pushing to a Remote After Resetting # If you have already pushed the commits you just reset, a normal git push will be rejected because you have rewritten history. You will need to force push:\ngit push --force-with-lease origin main --force-with-lease is safer than --force because it prevents accidentally overwriting commits that someone else may have pushed.\nConclusion # git reset --soft is one of the most useful tools in Git — whether you want to clean up your commit history, combine multiple commits into one, or simply fix a mistake without losing your work. I hope these tips make your day-to-day coding a little smoother.\n","date":"12 March 2026","externalUrl":null,"permalink":"/en/posts/git-reset-soft-head-tips/","section":"Posts","summary":"","title":"How to Edit Git Commits (git reset --soft)","type":"posts"},{"content":"","date":"12 March 2026","externalUrl":null,"permalink":"/en/tags/tips/","section":"Tags","summary":"","title":"Tips","type":"tags"},{"content":"","date":"9 March 2026","externalUrl":null,"permalink":"/en/tags/filter-repo/","section":"Tags","summary":"","title":"Filter-Repo","type":"tags"},{"content":"","date":"9 March 2026","externalUrl":null,"permalink":"/en/tags/gitmailmap/","section":"Tags","summary":"","title":"Gitmailmap","type":"tags"},{"content":"မြန်မာဘာသာဖြင့် ဖတ်ရှုရန်\nIntroduction # Everyone who uses GitHub wants to see those green contribution graphs on their profile. But sometimes, you might be writing code and pushing every day, yet your contributions don\u0026rsquo;t show up on GitHub.\nThe main reason for this is that the email configured in your local Git config is different from the email associated with your GitHub account.\nIn this post, I will share why this happens and how you can update your past commits using gitmailmap and git filter-repo.\nWhy are Commits Missing? # 1. Check Local and GitHub Emails # First, let\u0026rsquo;s check why your commits are not appearing.\nRun the following command in your terminal to check the email set up in your local Git config:\ngit config user.email The email that appears there must match the email you added in GitHub under Settings -\u0026gt; Emails. That email also needs to be \u0026ldquo;Verified\u0026rdquo;.\n2. Check if Commits are Pushed to GitHub # If you only commit locally and haven\u0026rsquo;t run git push, it won\u0026rsquo;t appear in your contributions. Also, if you commit to a branch that isn\u0026rsquo;t the default branch (usually main or master) and haven\u0026rsquo;t merged it, it might not show up. Furthermore, in private repositories, contributions might be hidden if the setting to show private contributions on your profile is turned off.\nWhat if the Emails are Different? # If you\u0026rsquo;ve confirmed that the emails are different, you first need to update to the correct email for your future commits.\ngit config --global user.name \u0026#34;Your Name\u0026#34; git config --global user.email \u0026#34;your-real-email@example.com\u0026#34; Once you change this, all your new commits will automatically appear as contributions on GitHub. If you want to keep this specific to the current project, use it without --global.\nHow to Edit Past Commits? # The problem lies with the old commits made before you changed the email. They already contain the incorrect email. If you want to replace them with the correct email and name without changing the date-time, you need to use .mailmap together with git filter-repo.\n1. Install git filter-repo # git filter-repo is a tool that is faster and safer than Git\u0026rsquo;s built-in filter-branch.\nOn macOS: brew install git-filter-repo On Linux or Windows: pip install git-filter-repo Before doing anything, I highly recommend making a backup of your repository locally.\n2. Create a .mailmap File # Create a file named .mailmap in the root directory of your project. Insert the following format into that file:\nKyaw Kyaw Myo \u0026lt;your-real-email@example.com\u0026gt; \u0026lt;user@github.com\u0026gt; Here:\nReplace Kyaw Kyaw Myo with your real name Replace your-real-email@example.com with your verified email on GitHub Replace user@github.com with the incorrect email you previously used. 3. Rewrite History # Once you have saved the .mailmap file, run the following command:\ngit filter-repo --mailmap .mailmap This command will read through your commit history and replace the incorrect emails with the correct ones based on the information in .mailmap. The original commit dates will remain unchanged.\nForce Push # Because you have rewritten the commit history, a simple git push will no longer work. You need to do a force push.\ngit push --force-with-lease --all origin git push --force-with-lease --tags origin Since this overwrites the repo\u0026rsquo;s history, you should only do this for personal repos where you are the only contributor. If this is a team project, beware that it can impact other team members who have cloned the project.\nConclusion # After doing a force push and waiting a moment, if you check your GitHub profile, you will see the missing green blocks reappear. I hope this post is helpful for developers who are frustrated by their missing contribution graphs.\n","date":"9 March 2026","externalUrl":null,"permalink":"/en/posts/github-contributions-missing-gitmailmap/","section":"Posts","summary":"","title":"What to Do If GitHub Contributions Are Missing (gitmailmap)","type":"posts"},{"content":"မြန်မာဘာသာဖြင့် ဖတ်ရှုရန်\nIntroduction # When pushing code, writing clear commit messages is very important. But sometimes, you might make a typo in the commit message or want to change it to something clearer.\nIn this post, I will share how you can update your past commits using git commit --amend and git rebase.\nUpdating the Latest Commit? # If the commit you want to edit is the latest commit, it\u0026rsquo;s the easiest one.\nYou can simply use the git commit --amend command.\ngit commit --amend Running this command will open a text editor in your terminal. You can write the new commit message and save it then.\nPro Tip: If you forgot to include some files in the last commit and just want to add them without changing the commit message, you can easily do so by running git add . followed by git commit --amend --no-edit.\nUpdating Older Commits? # If the commits you want to edit are not the latest ones, you\u0026rsquo;ll need to use git rebase. For example, if you want to edit the last 3 commits, you can use interactive rebase as follows:\ngit rebase -i HEAD~3 When you run this command, an editor (maybe Vim) will open, showing a list of commits.\nExample:\npick e3a1b35 Commit message 1 pick 7ac9a67 Commit message 2 pick 4db8c21 Commit message 3 You need to change the word pick to reword in front of the commit you want to edit. In this example, reword is used for every commit.\nreword e3a1b35 Commit message 1 reword 7ac9a67 Commit message 2 reword 4db8c21 Commit message 3 After saving and exiting, the editor will reopen sequentially for each commit marked with reword. At each step, simply update the commit message to your liking and save.\nPro Tip: In interactive rebase, besides reword, there are other useful commands. For example:\nUse drop (or d) if you want to delete an unnecessary commit Use squash (or s) if you want to merge two commits into one Use edit (or e) if you want to modify not just the commit message, but the files as well Use git rebase --abort if you make a mistake during the rebase and want to stop and return to the original state. Safety Net # Don\u0026rsquo;t worry if you make a mistake or accidentally delete something while editing your commit history during a rebase. There is a command called git reflog.\ngit reflog Through this command, you can view the history of all the actions you\u0026rsquo;ve taken, along with their commit hashes. From there, you can easily go back to anywhere you want using git reset --hard \u0026lt;commit-hash\u0026gt;.\nPush to Remote # Once you have finished editing your commits, you need to push them back to the remote repository. However, since the commit history has changed, a simple git push will no longer work.\nIn this situation, you need to do a force push. To avoid affecting other people\u0026rsquo;s code and for safety, you should always use --force-with-lease.\ngit push --force-with-lease Conclusion # These commands are incredibly useful in an everyday development workflow. Being able to easily fix mistakenly written commits before code reviews helps keep the git history clean and clear.\n","date":"6 March 2026","externalUrl":null,"permalink":"/en/posts/update-past-commits/","section":"Posts","summary":"","title":"How to Update Past Commits","type":"posts"},{"content":"","date":"6 March 2026","externalUrl":null,"permalink":"/en/tags/version-control/","section":"Tags","summary":"","title":"Version-Control","type":"tags"},{"content":"","date":"9 January 2026","externalUrl":null,"permalink":"/en/tags/ai/","section":"Tags","summary":"","title":"Ai","type":"tags"},{"content":"","date":"9 January 2026","externalUrl":null,"permalink":"/en/tags/github-copilot/","section":"Tags","summary":"","title":"Github-Copilot","type":"tags"},{"content":"","date":"9 January 2026","externalUrl":null,"permalink":"/en/tags/react/","section":"Tags","summary":"","title":"React","type":"tags"},{"content":"","date":"9 January 2026","externalUrl":null,"permalink":"/en/tags/uuid/","section":"Tags","summary":"","title":"Uuid","type":"tags"},{"content":"မြန်မာဘာသာဖြင့် ဖတ်ရှုရန်\nIntro # With the rise of AI assistants, the software development landscape has changed significantly. Tools like GitHub Copilot can now handle everything from simple CRUD apps to complex logic implementation. Developers can now focus more on high-level requirements.\nIn this post, I want to share about the UUID Generator project that I built using GitHub Copilot.\nUUID Generator Features # UUID Generator is built with React 19, Vite, and Tailwind CSS. The main features are:\nUUID Version Support: Supports V1 (timestamp-based), V4 (random), and V7 (time-ordered). Batch Generation: Can generate up to 200 UUIDs at a time. The UI is optimized to preview only 20 items for performance. Smart Controls: Batch size and preview count can be controlled with a unified slider. Copy \u0026amp; Download: Supports clipboard copy and text file download (with timestamped filename). Formatting: Supports uppercase, removing hyphens, and wrapping braces. Breaking Down # 1. AI-First Development # The unique aspect of this project is that almost all core implementation was written by GitHub Copilot (GPT-5.1-Codex). Human inputs were limited to high-level requirements, code review, and testing guidance. React architecture, Tailwind styling, business logic, and unit tests were fully implemented by GitHub Copilot.\n2. Prompt Evolution # The workflow didn\u0026rsquo;t follow the traditional planning -\u0026gt; coding pattern but rather a conversational iteration approach. If you look at the prompt history documented in the README, you can see how features were built incrementally:\nInitial Interface styling Version selector logic Mobile responsive badges UX interactions (copy feedback) Scaling to 200 items Unified slider control Information architecture refinement 3. Technical Highlights # The implementation quality provided by Copilot is quite impressive.\nCustom Hooks: Logic is separated into useTheme and useUuidGenerator, making the code clean and easy to test. Web Crypto API: Uses crypto.randomUUID() via the Web Crypto API instead of Math.random() for true randomness. Optimized Rendering: Includes logic to render only items in the viewport instead of rendering all 200 items. Conclusion # This project is a good example for testing the AI-assisted development workflow. It allowed me as a developer to focus more on architectural vision and quality assurance rather than implementation details. You can explore the project source code on GitHub and try out the Demo.\n","date":"9 January 2026","externalUrl":null,"permalink":"/en/posts/uuid-generator-with-ai-agent/","section":"Posts","summary":"","title":"UUID Generator With AI Agent","type":"posts"},{"content":"","date":"9 January 2026","externalUrl":null,"permalink":"/en/tags/vite/","section":"Tags","summary":"","title":"Vite","type":"tags"},{"content":"","date":"29 December 2025","externalUrl":null,"permalink":"/en/tags/github-actions/","section":"Tags","summary":"","title":"Github-Actions","type":"tags"},{"content":"မြန်မာဘာသာဖြင့် ဖတ်ရှုရန်\nIntro # In my previous post, I talked about setting up a linter workflow for Go projects. While linting is essential for maintaining code quality, testing is equally important to ensure your code works as expected and to make sure newly added features don\u0026rsquo;t break existing functionality.\nIn this post, I want to share the GitHub Actions workflow I use for unit testing in my current projects.\nAction File For Unit Tests # This is the workflow file I use to run unit tests and calculate code coverage for Go projects.\nname: Unit Tests on: push: branches: [main] paths: - \u0026#34;cmd/**\u0026#34; - \u0026#34;pkg/**\u0026#34; - \u0026#34;go.mod\u0026#34; - \u0026#34;go.sum\u0026#34; - \u0026#34;Makefile\u0026#34; pull_request: branches: [main] paths: - \u0026#34;cmd/**\u0026#34; - \u0026#34;pkg/**\u0026#34; - \u0026#34;go.mod\u0026#34; - \u0026#34;go.sum\u0026#34; - \u0026#34;Makefile\u0026#34; jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@v6 with: go-version: \u0026#34;1.25.5\u0026#34; # Caching prevents redownloading dependencies on every run until # go.mod/go.sum changes - name: Cache Go modules uses: actions/cache@v5 with: path: | ~/go/pkg/mod # Go modules cache ~/.cache/go-build # Go build cache key: ${{ runner.os }}-go-${{ hashFiles(\u0026#39;**/go.sum\u0026#39;) }} - name: Run unit tests run: make test - name: Run tests with coverage run: make test-coverage - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: files: ./coverage.out flags: unittests fail_ci_if_error: false Breaking Down # 1. Staring on: # Similar to the linter workflow, this workflow runs whenever code is pushed to the main branch or a pull request is created. It ensure the workflow only runs when necessary, Go source files (cmd/**, pkg/**), build-related and dependency files (Makefile, go.mod, go.sum), saving GitHub Actions minutes.\n2. Setup and Caching # This job runs on ubuntu-latest and sets up Go v1.25.5. As mentioned in the previous post, the caching step is a key. It reuses the Go modules and build cache as long as go.sum hasn\u0026rsquo;t changed, significantly reducing the workflow execution time.\n3. Running Tests # This workflow relies on the Makefile to execute the test commands. You\u0026rsquo;ll need to have the corresponding commands defined in your project\u0026rsquo;s Makefile:\nmake test: Runs standard unit tests (e.g., go test ./...). make test-coverage: Runs tests and generates a coverage report file (coverage.out). 4. Uploading Test Coverage # - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: files: ./coverage.out flags: unittests fail_ci_if_error: false After the tests run, it upload the results to Codecov to visualize the coverage result. It point to the coverage.out file generated in the step #4 with files: ./coverage.out. Setting fail_ci_if_error: false ensures that if there\u0026rsquo;s an issue uploading to Codecov, the entire CI workflow doesn\u0026rsquo;t fail as long as the tests themselves passed.\nConclusion # By adding this test workflow, you can have more confidence that your code won\u0026rsquo;t break existing features when you submit a PR. Looking at the test coverage report also helps you identify which parts of your project still need unit tests. Testing is an indispensable part of any CI/CD pipeline, and I highly recommend setting it up for your projects.\n","date":"29 December 2025","externalUrl":null,"permalink":"/en/posts/unit-test-with-github-actions/","section":"Posts","summary":"","title":"Unit Test With GitHub Actions","type":"posts"},{"content":"မြန်မာဘာသာဖြင့် ဖတ်ရှုရန်\nIt\u0026rsquo;s been almost a year since my last blog post, which was back in January. I\u0026rsquo;ve been busy with personal things and also just feeling lazy.\nRecently, I decided to try out Bubble Tea and also wanted to write a small CLI app to calculate personal income tax in Myanmar. That led to the PIT Calculator CLI app. You can try it with both a simple CLI interface and a TUI interface. With the help of Copilot, I managed to write a proper README, so I encourage you to check it out if you have a chance. I still need to add features for life insurance, other tax exemptions, bonuses, and other taxable income types.\nIntro # In software development, code quality and consistency are important. In team projects, everyone writes code in their own style. This can lead to hard-to-read code, bugs, unreachable code, and inconsistent style. To help prevent these problems, especially when in PRs and merging code into stable branches, linting is important for any software projects.\nIn this post, I explain the GitHub Actions workflow I use for linting in the PIT Calculator CLI app. I recently started using GitHub Actions for my personal projects, except for Hugo deployment workflows.\nWorkflow For Lint # This is the lint workflow for Go I\u0026rsquo;m using currently.\nname: Lint permissions: contents: read on: push: branches: [main] paths: - \u0026#34;cmd/**\u0026#34; - \u0026#34;pkg/**\u0026#34; - \u0026#34;go.mod\u0026#34; - \u0026#34;go.sum\u0026#34; - \u0026#34;.github/workflows/lint.yml\u0026#34; pull_request: branches: [main] paths: - \u0026#34;cmd/**\u0026#34; - \u0026#34;pkg/**\u0026#34; - \u0026#34;go.mod\u0026#34; - \u0026#34;go.sum\u0026#34; - \u0026#34;.github/workflows/lint.yml\u0026#34; jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@v6 with: go-version: \u0026#34;1.25.5\u0026#34; # Caching prevents redownloading dependencies on every run until # go.mod/go.sum changes - name: Cache Go modules uses: actions/cache@v5 with: path: | ~/go/pkg/mod # Go modules cache ~/.cache/go-build # Go build cache key: ${{ runner.os }}-go-${{ hashFiles(\u0026#39;**/go.sum\u0026#39;) }} # Ensures gofmt sees only actual Go code that belongs to your module - name: Run go fmt run: | FILES=$(gofmt -s -l $(go list -f \u0026#39;{{.Dir}}\u0026#39; ./...)) if [ -n \u0026#34;$FILES\u0026#34; ]; then echo \u0026#34;❌ Formatting issues found:\u0026#34; echo \u0026#34;$FILES\u0026#34; exit 1 fi echo \u0026#34;✅ Code formatting is correct\u0026#34; - name: Run go vet run: | go vet ./... echo \u0026#34;✅ Code analysis passed\u0026#34; - name: Check for imports run: | go mod tidy if [ -n \u0026#34;$(git diff --name-only go.mod go.sum)\u0026#34; ]; then echo \u0026#34;❌ go.mod/go.sum are not tidy. Run \u0026#39;go mod tidy\u0026#39; to fix:\u0026#34; git diff exit 1 fi echo \u0026#34;✅ Dependencies are tidy\u0026#34; Breaking Down # 1. Staring on: # on: push: branches: [main] paths: - \u0026#34;cmd/**\u0026#34; - \u0026#34;pkg/**\u0026#34; - \u0026#34;go.mod\u0026#34; - \u0026#34;go.sum\u0026#34; - \u0026#34;.github/workflows/lint.yml\u0026#34; pull_request: branches: [main] paths: - \u0026#34;cmd/**\u0026#34; - \u0026#34;pkg/**\u0026#34; - \u0026#34;go.mod\u0026#34; - \u0026#34;go.sum\u0026#34; - \u0026#34;.github/workflows/lint.yml\u0026#34; This on: section defines when the workflow runs:\nIt runs on both push and pull_request. branches: [main] means it runs only when pushing or creating a PR to the main branch. paths: means it runs only when files under cmd/**, pkg/**, or the workflow file (lint.yml) change. This avoids running unnecessarily and saves GitHub Actions minutes. 2. Preparing Environment And Caching # runs-on: ubuntu-latest: The job runs on an Ubuntu latest environment. uses: actions/setup-go@v6: Installs Go v1.25.5 on the runner. actions/cache@v5: Caches Go modules and build cache so that dependencies don’t need to be downloaded again when go.mod and go.sum don’t change. 3. Code Formatting # FILES=$(gofmt -s -l $(go list -f \u0026#39;{{.Dir}}\u0026#39; ./...)) if [ -n \u0026#34;$FILES\u0026#34; ]; then echo \u0026#34;❌ Formatting issues found:\u0026#34; echo \u0026#34;$FILES\u0026#34; exit 1 fi echo \u0026#34;✅ Code formatting is correct\u0026#34; gofmt -s -l: Lists Go files that are not formatted correctly. if [ -n \u0026quot;$FILES\u0026quot; ]; then: If $FILES is not empty, the workflow exits with a failure (exit 1). This prevents unformatted code from reaching the main branch. 4. Static Code Analysis # go vet ./...: It checks for common mistakes such as format string issues like using %d format specifier instead of %s, unused variables, incorrect struct tag, unreachable code, and unsafe conversions.\n5. Checking Unused Dependencies # go mod tidy if [ -n \u0026#34;$(git diff --name-only go.mod go.sum)\u0026#34; ]; then echo \u0026#34;❌ go.mod/go.sum are not tidy. Run \u0026#39;go mod tidy\u0026#39; to fix:\u0026#34; git diff exit 1 fi echo \u0026#34;✅ Dependencies are tidy\u0026#34; go mod tidy: Removes unused dependencies and adds missing ones. if [ -n \u0026quot;$(git diff --name-only go.mod go.sum)\u0026quot; ]; then: The workflow runs git diff on go.mod and go.sum. If there are changes, it means the developer did not run go mod tidy locally before push. In that case, the workflow fails with exit 1. Conclusion # This linting workflow cannot check 100% of code quality issues. It helps pull request review and catches many common problems. But reviewers still need to review design patterns and cognitive complexity carefully. I may write about more useful workflows soon. You can also explore the repo in my GitHub for examples of other workflows.\n","date":"16 December 2025","externalUrl":null,"permalink":"/en/posts/go-linter-with-github-actions/","section":"Posts","summary":"","title":"Go Linter With GitHub Actions","type":"posts"},{"content":"","date":"22 January 2025","externalUrl":null,"permalink":"/en/tags/development/","section":"Tags","summary":"","title":"Development","type":"tags"},{"content":"မြန်မာဘာသာဖြင့် ဖတ်ရှုရန်\nMany compiled languages like Go don’t natively support hot reloading. Flutter is an exception. For developers, the typical process of writing code and then manually recompiling in the terminal might seem fine at first. However, as the frequency increases, it can become quite frustrating. For people like me, who tend to use a code editor and terminal separately, it’s probably even worse.\nThe main pain point here is context switching. When working on complex code with nested iterations during development or debugging, it becomes challenging to maintain focus.\nWhen you need to compile and test your code inside a Docker container, the process becomes even more cumbersome. Mounting your current codebase as a volume into the container adds extra steps and complexity to the development workflow.\nAir for Hot Reloading # I use Air for live reloading. It’s just that an ex-coworker introduced me to it, and I’ve been using it ever since. While there are some drawbacks, I’m currently using Air for the image processing server I’m working on.\nInstalling Air is easy. Run this command to install it:\ngo install github.com/air-verse/air@latest Set up a configuration file, .air.toml, and start your project with this command:\nair -c .air.toml You can initialize the .air.toml file automatically with:\nair init Air in Dockerfile # Since I often run projects with Docker, I install Air directly in the Dockerfile and use Docker Compose to mount the codebase directory for live reloading.\nDockerfile\nFROM golang:1.23.4-bookworm WORKDIR /app RUN go install github.com/air-verse/air@latest COPY . . RUN go mod download CMD [\u0026#34;air\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;.air.toml\u0026#34;] docker-compose.yaml\nservices: app: build: context: . dockerfile: Dockerfile env_file: - .env ports: - 8080:8080 volumes: - ./:/app At the time of writing this post, the latest release of Air doesn’t support Go versions below 1.23. If, for some reason, you’re using an older Go version, you’ll need to install an earlier version of Air that’s compatible.\nLast But Not Least # I’d recommend setting a slightly higher value for the rerun_delay in the .air.toml file. Without this adjustment, Air might unnecessarily trigger builds immediately after saving code changes.\n","date":"22 January 2025","externalUrl":null,"permalink":"/en/posts/hot-reload-in-go/","section":"Posts","summary":"","title":"Hot Reload in Go","type":"posts"},{"content":"မြန်မာဘာသာဖြင့် ဖတ်ရှုရန်\nDevelopers who work on mobile and web apps, those who have written CRUD APIs, and who have worked with third-party APIs are likely to be very familiar with pagination. If you’re building an API, you might add query parameters like limit, and skip to the URL. As part of the validation, you might set a maximum value for limit, and so on.\nFor mobile developers, infinite scroll often require triggering the next API call when the user scrolls to a certain point. Since these concepts are likely well-known to most of you, I’ll skip straight to the main point.\nOffset Pagination # The reason of discussing offset pagination before cursor pagination is that it’s one of the most widely used methods. It works in a simple way: by providing query parameters like ?limit=20\u0026amp;skip=40, it skips the first 40 records in a sorted dataset and returns the subsequent 20 records.\nIt can be written in SQL like below:\nSELECT * FROM records OFFSET 20 LIMIT 20; It\u0026rsquo;s similar in MongoDB too.\ndb.records.find({}, { limit: 20, skip: 20 }) Drawbacks of Offset Pagination # Offset pagination has two main drawbacks:\n1. Performance Issue on Large Dataset # The first drawback is performance issue. When the dataset becomes large, using OFFSET can cause significant problems as it requires scanning the entire table to retrieve the data. This issue worsens with complex queries, potentially impacting the performance of the entire database. In a microservices architecture, such queries can even affect the overall service performance.\n2. Inconsistent Data When Insert/Delete # The second drawback is the inability to provide consistent data. This issue may not be noticeable in systems with infrequent changes. However, for systems like social media apps, B2C, or C2C marketplace apps, the inability to deliver consistent data can become a significant problem.\nFor example, imagine a database table containing 100 records with IDs ranging from 1 to 100, and you want to fetch data in descending order. On the first request,you would receive 20 records with IDs from 100 to 81 by using OFFSET 0 LIMIT 20. On the second request, using OFFSET 20 LIMIT 20, you would get the next 20 records with IDs from 80 to 61. So far, everything seems to be working fine.\nNow, imagine that before the second request, a new record with ID 101 is added to the table. This is where the problem begins. Since the second query uses OFFSET 20 LIMIT 20, it skips 20 records starting from ID 101, which results in skipping records from IDs 101 to 82 and returning records from IDs 81 to 62. As a result, the record with ID 81 gets included again in the second set, causing duplicates.\nIn scenarios like infinite scroll, this duplication can lead to a poor user experience where the same record is displayed twice. In small teams with only one QA tester, this issue can sometimes go unnoticed during the testing phase.\nThe same issue arises if a record is deleted during pagination. For example, if the record with ID 100 is deleted before the second query, the result changes. Using OFFSET 20 LIMIT 20 will now return records from ID 79 to 60, completely skipping ID 80. This issue can occur with both hard deletes and soft deletes that utilize a deleted_at column.\nCursor Pagination # Cursor pagination is different from offset pagination. It uses identifiers like ULID or timestamps such as created_at to paginate through data.\nIt can be written in SQL like below:\nSELECT * FROM records WHERE created_at \u0026lt; 1736533304 LIMIT 20; It\u0026rsquo;s similar in MongoDB too.\ndb.records.find( { created_at: { $lt: 1736533304 } } ).limit(20); Pros of Cursor Pagination # Since condition-based query can be used, the issues encountered in offset pagination are effectively resolved.\n1. Efficient Queries and Index Utilization # Since OFFSET is no longer used, there is no need to scan the entire table or collection, resulting in improved query performance.\nWith condition-based skipping, it becomes possible to create indexes specifically for pagination. Whether it’s SQL or NoSQL, indexes cannot be created for OFFSET, but they can be utilized effectively in this approach.\n2. Consistent Data When Insert/Delete # With cursor pagination, issues like missing or duplicated data caused by frequent inserts or deletes in offset pagination are resolved. This approach ensures consistent data retrieval, regardless of how frequently data is inserted or deleted, eliminating the possibility of data loss.\nHowever, in systems where data additions or deletions occur rarely, it might not be necessary to consider these issues as a priority.\nDrawbacks of Cursor Pagination # Cursor pagination is not a silver bullet, and it comes with its own limitations.\n1. Complexity # Cursor pagination is more complex to implement compared to offset pagination. It requires additional calculations and considerations. For instance, if you prefer not to use an AUTO_INCREMENT ID or cannot rely on the created_at column due to some reasons, you might need to adopt alternatives like UUID v7. Furthermore, if the cursor provided by the client is invalid and no data is returned from database, you’ll need to handle the business logic to decide which data to send back.\n2. Inflexibility # Cursor pagination works well when navigating sequentially. However, it’s not suited for jumping directly between pages (e.g., from page 1 to page 5). Achieving this requires additional workarounds, and even then, the solution might not be efficient.\nAnother drawback is that reverse navigation can be challenging. For instance, if you’re on page 5 and want to go back to page 4, it’s not straightforward. To handle this, you’d need to store the data for previous pages locally, such as in local storage.\nThese issues are less significant in scenarios like infinite scroll, where sequential loading of data is the norm.\n3. Depends on Sort and Order By # This limitation ties into the inflexibility issue. For example, if you’re on page 5 and decide to sort by a different column, such as the name column, or switch from ascending to descending order, it’s not feasible with cursor pagination. Even if you manage to implement such functionality, it won’t be efficient. While there are workarounds to achieve these tasks, they often come with compromises, which is why it’s important to highlight this upfront.\n4. Misc # Another consideration, though rare, is columns used for sorting must be unique. While this isn’t an issue for fields like ID, it can raise a problem for columns like created_at, where duplicate values are possible, especially down to the millisecond level.\nConclusion # In summary, there’s no universal silver bullet solution when it comes to pagination. The choice depends heavily on the nature of the system and its requirements.\nFor instance, if you decide to use cursor pagination because your API is primarily for mobile use, you might run into issues when scaling to a web version later. On the other hand, relying on offset pagination, becuase inserts and deletes are infrequent, could lead to performance bottlenecks if those operations increase over time.\nIn consumer apps, enforcing a force update for users is more challenging than it might seem. The larger your user base, the more complex these decisions become. When facing such scenarios, introducing API versioning can be an effective way to handle the transition and ensure compatibility across different use cases.\n","date":"12 January 2025","externalUrl":null,"permalink":"/en/posts/cursor-pagination/","section":"Posts","summary":"","title":"Cursor Pagination","type":"posts"},{"content":"","date":"12 January 2025","externalUrl":null,"permalink":"/en/tags/mongodb/","section":"Tags","summary":"","title":"Mongodb","type":"tags"},{"content":"","date":"12 January 2025","externalUrl":null,"permalink":"/en/tags/nosql/","section":"Tags","summary":"","title":"Nosql","type":"tags"},{"content":"","date":"12 January 2025","externalUrl":null,"permalink":"/en/tags/postgres/","section":"Tags","summary":"","title":"Postgres","type":"tags"},{"content":"","date":"12 January 2025","externalUrl":null,"permalink":"/en/tags/sql/","section":"Tags","summary":"","title":"Sql","type":"tags"},{"content":"","date":"8 January 2025","externalUrl":null,"permalink":"/en/tags/python/","section":"Tags","summary":"","title":"Python","type":"tags"},{"content":"","date":"8 January 2025","externalUrl":null,"permalink":"/en/tags/regex/","section":"Tags","summary":"","title":"Regex","type":"tags"},{"content":"မြန်မာဘာသာဖြင့် ဖတ်ရှုရန်\nPart 1\nPipe Metacharacter # Aside from the metacharacters discussed in Part 1, ., ?, +, *, ^, and $, another useful one is the |.\nimport re print(re.findall(r\u0026#39;Go|Python\u0026#39;, \u0026#39;I\\\u0026#39;m intrested in Go, JavaScript, Python, and SQL\u0026#39;)) Just like in other programming languages, the | can be seen as an OR operator. In the Python code above, since it matches either Go or Python, the result will be ['Go', 'Python'].\nCharacter Classes # Square brackets [] are used in regular expressions to define sets and ranges.\nA set allows you to specify a collection of characters to match. For example, [abc] matches any one of a, b, or c. A range lets you specify a range of characters. For example, [a-z] matches any lowercase letter from a to z. [0-9] matches any digit from 1 to 9. Set # import re print(re.findall(r\u0026#39;[nl]ot\u0026#39;, \u0026#39;Not not Hot hot Lot lot\u0026#39;)) In the above code, r'[nl]ot' specifies that the string must start with n or l, followed by ot. As a result, the matches will be ['not', 'lot'].\nIf you want the [nl] expression to be case-insensitive, the pipe character, |, can be used to achieve this.\nimport re print(re.findall(r\u0026#39;[N|nL|l]ot\u0026#39;, \u0026#39;Not not Hot hot Lot lot\u0026#39;)) The result will be ['Not', 'not', 'Lot', 'lot']. Instead of using the | operator, you can achieve the same result by using the ignorecase flag to make the pattern case-insensitive as below.\nimport re print(re.findall(r\u0026#39;[nl]ot\u0026#39;, \u0026#39;Not not Hot hot Lot lot\u0026#39;, re.IGNORECASE)) Range # import re if re.fullmatch(r\u0026#39;[A-Za-z0-9]+\u0026#39;, \u0026#39;NoSpaceAndSpecialCharacter0123456789\u0026#39;): print(\u0026#39;Match\u0026#39;) else: print(\u0026#39;Not match\u0026#39;) Most of the programmers commonly use the above regular expressions which allow only alphabets and numbers, while excluding space and special characters. In this case, the code will execute print('Match'). However, if there are any space or special character in the string, it will execute print('Not match').\nNot In ^ # The ^ character, which is discussed in Part 1 as indicating the start of the string, is also used within sets and ranges to mean not in.\nimport re print(re.findall(r\u0026#39;[^A-Za-z0-9]+\u0026#39;, \u0026#39;NoSpaceAndSpecialCharacter#!0-0 123456789\u0026#39;)) print(re.findall(r\u0026#39;[^nl]ot\u0026#39;, \u0026#39;not hot lot\u0026#39;))k For the first print statement, the pattern matches anything that is not an alphabet or a digit. Hence, the result will be ['#!', '-', ' '].\nFor the second print statement, the pattern matches anything that does not start with n or l. The output will be ['hot'].\nGreedy and Non-Greedy Quantifiers # Greedy Quantifier # Standard quantifiers like ., ?, +, *, and {from, to} are greedy by default. Greedy means that they will try to match as much as possible while still satisfying the pattern. In other words, they match the longest possible string that fits.\nThough it may seem confusing, the following code will help clarify how greedy quantifier works:\nimport re print(re.findall(r\u0026#39;\\w+\u0026#39;, \u0026#39;abcdefh123!@#\u0026#39;)) When you run the code, the result will be as expected: ['abcdefh123']. When you change the pattern to r'\\d+', the result will be ['123'].\nThe behavior of the + quantifier is not always straightforward and can be a bit tricky when it comes to backtracking. Let’s break down the example step-by-step to better understand how it works:\nimport re print(re.findall(r\u0026#39;.*hello\u0026#39;, \u0026#39;xhello123\u0026#39;)) It may seem like .* matches the entire sentence, resulting in ['xhello123']. However, .* does match the whole sentence initially, but then it processes the remaining tokens one by one in backtracking. If this is unclear, I’ll explain it step by step below:\n.*: xhello123 .*h: xhello123 -\u0026gt; xhello123 -\u0026gt; xhello123 -\u0026gt; xhello123 -\u0026gt; xhello123 -\u0026gt; xhello123 -\u0026gt; xhello123 -\u0026gt; xhello123 .*hello: xhello I borrowed this example from DataCamp\u0026rsquo;s Regular Expression in Python course without hesitation. If you’re a beginner looking to learn Python and data engineering, I highly recommend DataCamp.\nNon-Greedy Quantifier # The non-greedy quantifier, also known as the lazy quantifier, differs from the greedy quantifier in that it matches the smallest possible portion of the string.\nimport re print(re.findall(r\u0026#39;\\w+?\u0026#39;, \u0026#39;abcdefh123!@#\u0026#39;)) When you run the code, the result will be as expected: \u0026lt;re.Match object; span=(0, 1), match='a'\u0026gt;.\nOutroduction # As mentioned in Part 1, mastering regular expressions requires a lot of practice. It’s essential for both gaining a deep understanding and improving your skills. I’ll share more about capturing groups and backreferences in future posts.\nReferences: # Regular Expression in Python by Data Camp regex101.com Set and Ranges by javascript.info ","date":"8 January 2025","externalUrl":null,"permalink":"/en/posts/regular-expressions-101-part-02/","section":"Posts","summary":"","title":"Regular Expressions 101 (Part 2)","type":"posts"},{"content":"မြန်မာဘာသာဖြင့် ဖတ်ရှုရန်\nPart 2\nIntroduction # ([A-Za-z])\\w+\\s+\\W\\D\nWhen you come across a regular expression like that, chances are a lot of programmers (myself included) have no clue what it means at first glance. And when it’s time to write a regex for validation, most of us probably head straight to RegExr or regex101. These days, though, we’ve got tools like ChatGPT and GitHub Copilot to help out.\nWhile taking a data-related course, I came across regular expressions. So, I decided to write this post as a way to help myself remember them. Even though this post uses Python for examples, the concepts of regular expressions are pretty much the same across other programming languages too.\nGeneral Tokens # At a basic level, knowing these tokens can help you match a lot of patterns. While there are other tokens as well, I’ve left them out because they’re not used very often.\n\\d: Matches any digit from 0 to 9. \\s: Matches space, tab, and new line characters. \\w: Matches any word character, including a to z, A to Z, 0 to 9, and _. .: Matches everything except the new line character. You might have noticed that the tokens are all lowercase. If these tokens are switchched to uppercase, they’ll behave in the opposite way:\n\\D: Matches any character except digits (0 to 9). \\S: Matches any character except space, tab, and new line. \\W: Matches any character except word characters (a to z, A to Z, 0 to 9, and _). Here’s the Python code for demonstration. When you run it, the result will be ['H', 'i', '1', '2', '3']. The first parameter of the re.findall() function is the regular expression.\nimport re print(re.findall(r\u0026#39;\\w\u0026#39;, \u0026#39;Hi 123\u0026#39;)) If the regular expression is changed to r'\\w\\w', the result will be ['Hi', '12']. This matches any two consecutive word characters. If it\u0026rsquo;s changed to r'\\w\\s\\d', it will return ['i 1']. This matches a word character \\w, followed by a space \\s, and then a digit \\d.\nIf you want to match exactly 9 digits, is it necessary to write \\d nine times? To match a range, such as 3 to 5 characters, how should it be written? In such cases, you need to use a quantifier.\nGeneral Quantifiers # Check below for the most commonly used and simplest quantifiers:\n?: Zero or one occurrence. *: Zero or more occurrences. +: One or more occurrences. {5}: Exactly five occurrences. {5,9}: Between five and nine occurrences. {5,}: five or more occurrences. import re print(re.findall(r\u0026#39;\\d+\u0026#39;, \u0026#39;Hi 123 4567 89101112\u0026#39;)) In this Python example, when you match one or more digits \\d+, the result will be ['123', '4567', '89101112'] because it matches consecutive digits.\nIf you match exactly 4 digits using r'\\d{4}', the result will be ['4567', '8910', '1112'], since it matches only sequences of exactly 4 digits.\nIf you use r'\\d{4,}', the result will be ['4567', '89101112'], as it matches sequences of 4 or more digits.\nOne important thing to note here is that quantifiers apply immediately to the left. The quantifier will only affect the character or token directly to its left. This means it will only apply to the character or token it’s placed next to, not to the whole pattern.\nimport re print(re.findall(r\u0026#39;1\\w+\u0026#39;, \u0026#39;1a1b1c1d1e1f1g\u0026#39;)) In this Python code, the result will be ['1a1b1c1d1e1f1g'], not ['1a', '1b', '1c', '1d', '1e', '1f', '1g']. This happens because the + quantifier applies to \\w immediately to the left of it. The quantifier doesn’t affect both 1 and \\w together. Instead, the regular expression matches a sequence where a 1 is followed by one or more word characters, and it continues matching the entire string as a single match.\nExcaping Special Character # import re print(re.findall(r\u0026#39;.\\s\u0026#39;, \u0026#39;This is first sentence. And this is second sentence.\u0026#39;)) In this Python code, the intention is to check if there’s a full stop followed by a space. However, since the dot . is a special character in regular expressions to match any character, the result will be ['s ', 's ', 't ', '. ', 'd ', 's ', 's ', 'd ']. This happens because . matches any character, so it’s matching every occurrence of a character followed by a space.\nTo specifically match a full stop followed by a space, you need to escape the dot by using \\. So, using r'\\.\\s' will give the correct result: ['. ']. This ensures that the dot is treated literally as a full stop rather than a wildcard character.\nLast But Not Least # Two other useful tokens are ^ and $:\n^: Matches the start of the string. $: Matches the end of the string. import re print(re.findall(r\u0026#39;hello_\\d+\u0026#39;, \u0026#39;hello_world hello_123\u0026#39;)) In this Python code, the result will be ['hello_123']. If you want to check if the string starts with the pattern hello_ followed by one or more digits, you should use the pattern r'^hello_\\d+'.\nSimilarly, if you want to check if the string ends with hello_ followed by one or more digits, you should use the pattern r'hello_\\d+$'. The ^ ensures the string starts with the pattern, and the $ ensures it ends with the pattern.\nPython Functions # Since this post focuses mainly on regular expressions, I’ve used the re.findall() function. However, you can also experiment with the following Python functions depending on the specific needs:\nre.match(): Checks for a match only at the start of the string. re.search(): Searches the entire string for the first match. re.sub(): Replaces occurrences of a pattern with a specified string. re.split(): Splits the string at each match of the pattern. Outroduction # As the title, Regular Expression 101, mastering regular expressions requires plenty of practice. I recommend experimenting with matching patterns like email addresses and ID numbers to strengthen your skills. In future posts, I’ll cover more complex patterns, such as password validation, to help you tackle even more challenging use cases.\nReferences: # Regular Expression in Python by Data Camp regex101.com ","date":"20 December 2024","externalUrl":null,"permalink":"/en/posts/regular-expressions-101/","section":"Posts","summary":"","title":"Regular Expressions 101","type":"posts"},{"content":"","date":"6 November 2024","externalUrl":null,"permalink":"/en/tags/blowfish/","section":"Tags","summary":"","title":"Blowfish","type":"tags"},{"content":"","date":"6 November 2024","externalUrl":null,"permalink":"/en/tags/hugo/","section":"Tags","summary":"","title":"Hugo","type":"tags"},{"content":"မြန်မာဘာသာဖြင့် ဖတ်ရှုရန်\nWhat is Hugo? # Here’s an excerpt from the official Hugo documentation introduction.\nHugo is a static site generator written in Go, optimized for speed and designed for flexibility. With its advanced templating system and fast asset pipelines, Hugo renders a complete site in seconds, often less.\nFor a quick introduction, you can check out Hugo in 100 Seconds from Fireship. Why Hugo? # Hugo offers a variety of powerful features, including support for multilingual, templates, rich content formats, easy-to-use shortcodes, and image processing.\nI’ve written a few articles on Medium before, but this is my first time starting a blog. The main reason is that I tend to forget solutions to problems I’ve solved after some time.\nWhen I decided to start a blog, I hadn’t used any SSGs before. After a bit of research, I chose Hugo. Here’s why: I’m more comfortable with Go, so working with Go’s text/template and html/template feels natural. Also, Hugo makes it easy to manage content in both Burmese and English.\nWhat is Blowfish? # Blowfish is a theme designed for Hugo that includes support for Tailwind CSS v3, automatic image resizing, integrated site search, and more. You can explore additional features in the Blowfish documentation.\nSetting Hugo on Local # Prerequisites # To run Hugo locally, you’ll optionally need Git, Go, and Dart Sass for specific use casese.\nGit is essential to use Hugo Modules, install a theme by using Git Submodule, and use GitHub Pages. Go is required to use the Hugo Modules. Installation # For the sake of simplicity, I recommend using Homebrew for installing on Mac:\nbrew install hugo If Homebrew is not installed yet, it can be set up using the following script:\n/bin/bash -c \u0026#34;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\u0026#34; Setting Up # Create a new site named personal-blog using these commands: hugo new site personal-blog cd personal-blog hugo mod init github.com/pyaethu-aung/personal-blog hugo mod tidy Download configuration files to complete the setup for Blowfish. Access the files from this link and copy them into the /config/_default directory. Add following content to config/_default/module.toml file: [[imports]] disable = false path = \u0026#34;github.com/nunocoracao/blowfish/v2\u0026#34; Start Hugo by running the command below. You can view the homepage by navigating to http://localhost:8080/: hugo server --port 8080 Home page after initial set up Configuration # Update the following values in /config/_default/languages.en.toml: title = \u0026#34;My Personal Blog\u0026#34; [params.author] name = \u0026#34;My Name\u0026#34; email = \u0026#34;hello@myname.com\u0026#34; headline = \u0026#34;I\u0026#39;m writing blog by using Hugo and Blowfish\u0026#34; bio = \u0026#34;Freelance Developer\u0026#34; links = [ { email = \u0026#34;mailto:hello@myname.com\u0026#34; }, { github = \u0026#34;https://github.com/myname\u0026#34; } ] You’ll see the homepage update accordingly: Home page after changing languages.en.toml Update the following values in /config/_default/menus.en.toml: [[main]] name = \u0026#34;Blog\u0026#34; pageRef = \u0026#34;posts\u0026#34; weight = 10 [[main]] name = \u0026#34;Tags\u0026#34; pageRef = \u0026#34;tags\u0026#34; weight = 30 Home page after changing menus.en.toml Update the following values in /config/_default/params.toml: [homepage] layout = \u0026#34;hero\u0026#34; homepageImage = \u0026#34;images/hero_background.jpg\u0026#34; # hero_background.jpg should be in \u0026#34;/assets/images/\u0026#34; showRecent = true showMoreLink = true cardView = true Home page after changing params.toml Add a New Post # Hugo also supports other formats like HTML and Pandoc. You can check out the details here. Add a new Markdown post by running the command below: hugo new content content/posts/my-first-post/index.md Update the front matter in the generated index.md file as shown below, and use sample content from Lorem Markdownum: +++ title = \u0026#34;My First Post\u0026#34; date = 2024-11-06T10:10:00+00:00 draft = false tags = [\u0026#34;hugo\u0026#34;, \u0026#34;blowfish\u0026#34;] slug = \u0026#34;my-first-post\u0026#34; +++ Update the following values in /config/_default/params.toml: [article] showAuthor = true Home page after adding a new post New post To add a hero section to the post and display the author information at the bottom of the page, update the following values in /config/_default/params.toml. Additionally, place a hero image in the same directory as index.md with the filename featured.jpeg. [article] showAuthor = true showAuthorBottom = true showHero = true heroStyle = \u0026#34;big\u0026#34; You will notice the post changes as shown below: New post with hero image The post list item on the home screen will appear as shown below: Home list item with hero image You can download the final project resulting from the steps above via this link.\nLater, I’ll write about hosting the blog on GitHub Pages and using Hugo’s multilingual mode to post in both English and Burmese.\n","date":"6 November 2024","externalUrl":null,"permalink":"/en/posts/personal-blog-using-hugo-and-blowfish/","section":"Posts","summary":"","title":"Personal Blog Using Hugo and Blowfish","type":"posts"},{"content":"","externalUrl":null,"permalink":"/en/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/en/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","externalUrl":null,"permalink":"/en/series/","section":"Series","summary":"","title":"Series","type":"series"}]