Slonik and CockroachDB
By Morgan Winslow at
Testing out Slonik, "a Node.js PostgreSQL client with runtime and build time type safety", on CockroachDB
Overview
In this blog I'll be looking at some of the basics of Slonik and confirming that they integrate smoothly with CockroachDB. As you'll see in the About section below, there's a lot of cool features of Slonik, but I won't be diving into all of them today. Instead I'll be going through some standard CRUD operations and basic Slonik features.
About Slonik
Slonik is A PostgreSQL client built for Node.js with an emphasis on runtime type safety. As with many things, if it works with Postgres then I want to try it out with CockroachDB. The repo can be found here.
Some core principals and features are:
- Promotes writing raw SQL.
- Discourages ad-hoc dynamic generation of SQL.
- Safe connection handling
- Safe transaction handling
- Detailed logging
- Type safety
One of the other big reasons for the creation of Slonik was "motivation to reduce the repeating code patterns". This is a bit different than my last couple blogs which have been more ORM/SQL generation heavy and is not at all the focus of Slonik.
Further Reading
The founder of Slonik has some great blogs about the birth of Slonik and the reasons for wanting to create a Node.js client for Postgres.
- Processing large volumes of data safely and fast using Node.js and PostgreSQL
- Lessons learned scaling PostgreSQL database to 1.2bn records/month
Pre-reqs
- A CockroachDB connection string. I recommend using the Serverless offering for a quick start. You can follow the steps outlined here
- npm install slonik
- npm install zod
Create person Table in CockroachDB
First, let's create a very simple table in CockroachDB that we will use to do our testing. Note, it's not usually recommended to use ints as a Primary Key in CockroachDB, but we'll be using this for the sake example.
CREATE TABLE person (id INT PRIMARY KEY, name STRING);
Create Connection
The first thing we'll do is create a connection to the database using the createPool
func.
import {
createPool,
} from 'slonik';
const pool = await createPool('postgres://username:password@your-serverless-123.g8z.cockroachlabs.cloud:26257/defaultdb?sslmode=require');
pool.connect(async (connection) => {
await connection.query(sql.typeAlias('id')`SELECT 1 AS id`);
});
In this example, the connection will be kept alive until the Promise is resolved. You can also use pool
directly, which is what I'll be doing most of the time in my examples for quick testing.
pool.query(sql.typeAlias('id')`SELECT 1 AS id`);
Define Objects
The first object we'll create is a personObject
that represents our person
table. You'll notice that "Slonik integrates zod to provide runtime query result validation and static type inference".
const personObject = z.object({
id: z.number(),
name: z.string(),
})
const sql = createSqlTag({
typeAliases: {
id: z.object({
id: z.number(),
}),
void: z.object({}).strict(),
}
})
The second object is a Type Alias that utilizes a sql tag
.
INSERT
We'll take a look at both a simple INSERT, as well as bulk INSERT. Both of these worked on CockroachDB without any adjustments.
Simple INSERT
In this example we are just inserting an id and passing a value of morgan
for the name.
const morgan = 'morgan'
const personInsertQuery = sql.type(personObject)`INSERT INTO person (id, name) VALUES (1, ${morgan})`
await pool.query(personInsertQuery)
query
is one of the most basic query methods and is equivalent to node-postgres.
Bulk INSERT
await pool.query(sql.unsafe`
INSERT INTO person (id, name)
SELECT *
FROM ${sql.unnest(
[
[1, 'mauricio'],
[2, 'alex']
],
[
'int4',
'string'
]
)}
`);
SELECT
The first example here will just be a simple SELECT statement using our personObject
. The second will leverage one of the ways that Slonik reduces code patterns.
Simple SELECT with Object
const personSelectQuery = sql.type(personObject)`SELECT id, name FROM person WHERE name = ${morgan}`;
const morganSelect = await pool.any(personSelectQuery);
if (morganSelect.length > 0) {
console.log(morganSelect[0].id)
}
oneFirst SELECT
oneFirst
is one of the methods that abstracts typical coding patterns.
Returns value of the first column from the first row.
- Throws NotFoundError if query returns no rows.
- Throws DataIntegrityError if query returns multiple rows.
- Throws DataIntegrityError if query returns multiple columns.
try {
const selectOneFirst = await pool.oneFirst(sql.type(personObject.shape.id)`
SELECT id
FROM person
WHERE name = ${morgan}
`);
console.log(selectOneFirst)
} catch (error) {
console.log(error)
}
DELETE
The main difference in the DELETE case is you'll notice that I'm using the void
type alias.
await pool.query(sql.typeAlias('void')`DELETE FROM person WHERE id = ${morganSelect[0].id}`);
Conclusion
I did not have to make any changes to integrate basic Slonik features with CockroachDB. Since Slonik relies so heavily on raw SQL, rather than generation, I'm confident that this would remain the case in deeper tests.
Although this was just a quick compatibility test, it's clear there's plenty of other areas to explore here. Slonik seems to put things like connection pooling, transaction, and error handling at the forefront, which is a great fit for CockroachDB.
All code can be found here