Logo

dev-resources.site

for different kinds of informations.

SQL generation: Golang's builder pattern vs Clojure's persistent map

Published at
6/8/2024
Categories
sql
datamapping
orm
builder
Author
veer66
Categories
4 categories in total
sql
open
datamapping
open
orm
open
builder
open
Author
6 person written this
veer66
open
SQL generation: Golang's builder pattern vs Clojure's persistent map

I worked on a TODO code assignment for showing off my skills, and more importantly, showing my weak points. I coded in Golang and Masterminds/squirrel. Later, I ported only the SQL generation part to Clojure to compare and discuss why I prefer Clojure, which I have usually been asked about or even met with opposition for. I will discuss function by function and type by type. The first function is makeStatement.

func (repo *TodoRepoPg) makeStatement(orders []entity.Order, filters []entity.Filter) (string, []any, error) {
    builder := repo.Builder.Select("id, title, description, created, image, status")
    if err := buildOrders(&builder, orders); err != nil {
        return "", nil, err
    }
    if err := buildFilters(&builder, filters); err != nil {
        return "", nil, err
    }
    return builder.From("task").ToSql()
}
Enter fullscreen mode Exit fullscreen mode

The makeStatement function's name clearly indicates it utilizes the builder pattern. However, to improve readability and avoid cluttering the function with too many details, it delegates order and filter information building to separate functions: buildOrders and buildFilters. Next one is the make-statement function in Clojure with HoneySQL.

(defn make-statement [orders filters]
  (sql/format (merge {:select [:id :description :status]
                      :from [:task]}
                     (filters->map filters)
                     (orders->map orders))))
Enter fullscreen mode Exit fullscreen mode

In Clojure version, the main difference is that filters->map and orders->map are pure functions, which won't mutate or change their inputs like buildOrders and buildFilters do with the builder in Golang. The next one I will show contract or type or spec.

const (
    ID = iota
    Title
    Description
    Date
    Status
)

const (
    ASC = iota
    DESC
)

type Order struct {
    Field        int
    SortingOrder int
}

type Filter struct {
    Field int
    Value string
}
Enter fullscreen mode Exit fullscreen mode

In Golang, to complement function definitions, I define custom types for conveying order and filter information. While using strings for this purpose is also acceptable, I prefer using types to leverage Go's static analysis and prevent typos.

(s/def :db1/orders (s/coll-of (s/tuple #{:title :created :status} #{:+ :-})))
(s/def :db1/filters (s/coll-of (s/tuple #{:title :description} any?)))
Enter fullscreen mode Exit fullscreen mode

On the other hand, in Clojure, I defined similar contracts using Clojure Spec. Here, the information about orders and filters being collections of tuples resides within the Spec definition itself, unlike the separate function definitions in Golang.

func buildOrders(builder *squirrel.SelectBuilder, orders []entity.Order) error {
    for _, order := range orders {
        var fieldName string
        switch order.Field {
        case entity.Title:
            fieldName = "title"
        case entity.Date:
            fieldName = "created"
        case entity.Status:
            fieldName = "status"
        default:
            return fmt.Errorf("invalid field: %d", order.Field)
        }
        var sortOrder string
        switch order.SortingOrder {
        case entity.ASC:
            sortOrder = "ASC"
        case entity.DESC:
            sortOrder = "DESC"
        default:
            return fmt.Errorf("invalid sorting order: %d", order.SortingOrder)
        }
        orderExpr := fieldName + " " + sortOrder
        *builder = builder.OrderBy(orderExpr)
    }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

buildOrders looks very familiar. It reminds me of Pascal, which I learned 30 years ago. This suggests that the code utilizes a well-established approach, making it understandable to most programmers even without prior Go experience. However, I've identified potential code duplication between the type definition and the switch-case within this function.

(defn orders->map [orders] 
  (when-not (s/valid? :db1/orders orders)
    (throw (ex-info "Invalid input orders" (s/explain-data :db1/orders orders))))

  (->> orders
       (mapv #(let [[field order-dir] %] 
                [field (case order-dir
                         :+ :asc
                         :- :desc)]))
       (array-map :order-by)))
Enter fullscreen mode Exit fullscreen mode

The Clojure function orders->map might have surprised my younger self from 30 years ago. However, it leverages Clojure Spec to its full potential. Spec validates the input to the function, and provide clear explanations when validation fails. Furthermore, orders->map is a pure function, meaning it doesn't modify its input data. Both the input and output data leverage Clojure's persistent maps, a fundamental data structure known for immutability. Therefore,
unit testing for the orders->map function is relatively straightforward. I have no idea how to write a unit test for buildOrders in Go.

(deftest generate-orders-maps
  (is (= {:order-by []}
         (orders->map [])))
  (is (= {:order-by [[:title :desc]]}
         (orders->map [[:title :-]])))
  (is (= {:order-by [[:status :asc]]}
         (orders->map [[:status :+]])))
  (is (thrown-with-msg? Exception 
                        #"Invalid input orders"
                        (orders->map [[:id :+]]))))
Enter fullscreen mode Exit fullscreen mode

In conclusion, Go's main advantage lies in its familiarity for programmers from various languages like Pascal, Java, JavaScript, Python, and C. This familiarity extends to the builder pattern, which offers the additional benefit of auto-completion in IDEs and smart editors. On the other hand, Clojure and HoneySQL emphasize using data structures, especially persistent maps, for building queries.

While auto-completion is less important for Clojure programmers who are comfortable manipulating basic data structures, Clojure Spec offers significant advantages in data validation.

Spec can explain what happens when data fails to meet the requirements, promoting better error handling and adherence to the open-closed principle (where code can be extended without modifying existing functionality). Additionally, Clojure Spec is not part of the function definition itself, allowing for greater flexibility and potential separation of concerns.

More importantly, writing unit tests in Clojure with HoneySQL is significantly more efficient. Because orders->map is based on persistent data structures, it avoids modifying the input data. This immutability, along with the ease of comparing maps, makes them ideal for testing.

orm Article's
30 articles in total
Favicon
Let's take a quick look at Drizzle ORM
Favicon
Prisma 101 baby.
Favicon
The Step-by-Step Process of Online Reputation Repair for Personal and Business Profiles
Favicon
The Best Strategies for Removing Negative News Articles from Search Engines
Favicon
Trysil - Multi-tenat API REST
Favicon
Typing Prisma Json Fields? Yes, You Can!
Favicon
What is ORM
Favicon
Top 5 ORM Services to Safeguard Your Online Reputation in 2024
Favicon
Gorm Pagination With Ease
Favicon
Introdução ao ORM do Django: Exercícios Práticos
Favicon
Explorando ORM: Facilitando o Desenvolvimento com Bancos de Dados
Favicon
Techniques for Synchronous DB Access in TypeScript
Favicon
Why We Adopted a Synchronous API for the New TypeScript ORM
Favicon
Data Access with Dapper: A Lightweight ORM for .NET Apps
Favicon
Writing code like this improves efficiency by 100 times compared to directly using MyBatis
Favicon
The Power of Less: Streamlining Dependencies with Remult
Favicon
Seeking a Type-Safe Ruby on Rails in TypeScript, I Started Developing an ORM
Favicon
TypeORM: O ORM que Você Precisa Conhecer para Trabalhar com Node.js e TypeScript
Favicon
SQL generation: Golang's builder pattern vs Clojure's persistent map
Favicon
How to Setting Default UUID for User Model in Peewee
Favicon
Introduction to "Accel Record": A TypeScript ORM Using the Active Record Pattern
Favicon
Difference Between ORM and ODM
Favicon
How to add update, and delete data in peewee Database Python
Favicon
Most popular Postgres ORMs
Favicon
Introducing Stalactite ORM
Favicon
Advanced Django ORM Features (Q-Objects, F-Expressions, Aggregations and Annotations)
Favicon
Less Code (-75%) & More Power (💪) with Remult
Favicon
A Software Engineer's Tips and Tricks #1: Drizzle
Favicon
Asgard saviour: Neurelo
Favicon
Prisma vs. Drizzle: A Comprehensive Guide for Your NextJS Project

Featured ones: