Skip to content
/ sql Public

sql is another sql template string library. Bring your own database client.

License

Notifications You must be signed in to change notification settings

Synvox/sql

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

87 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@synvox/sql

sql is another SQL template string library

npm i @synvox/sql

Setting up for your client

@synvox/sql does not depend on a specific SQL client. You need to implement the Pool, PoolClient, and Client interfaces (exported from @synvox/sql) to use the library with the database drivers of your choice. Below is an example using pg:

import { connect, Pool, PoolClient, Client } from "@synvox/sql";
import * as pg from "pg";

// Example Pool implementation
class MyPool extends Pool {
  client: pg.Pool;

  constructor() {
    super();
    this.client = new pg.Pool();
  }

  query(query: string, values: any[]) {
    return this.client.query(query, values);
  }

  async isolate() {
    const pgClient = await this.client.connect();
    return new MyPoolClient(pgClient);
  }
}

// Example PoolClient implementation
class MyPoolClient extends PoolClient {
  client: pg.PoolClient;

  constructor(client: pg.PoolClient) {
    super();
    this.client = client;
  }

  query(query: string, values: any[]) {
    return this.client.query(query, values);
  }

  async release() {
    this.client.release();
  }
}

// Example Client implementation (no pooling)
class MyClient extends Client {
  client: pg.Client;

  constructor() {
    super();
    this.client = new pg.Client();
  }

  async query(query: string, values: any[]) {
    return this.client.query(query, values);
  }
}

// Creating a connection using pooling:
const sql = connect(new MyPool());

// or if you don't need pooling:
const sql2 = connect(new MyClient());

Executing Queries

exec()

Use exec to run queries that do not need to return rows or for multi-statement execution:

await sql`
  create table example (
    id serial primary key
  )
`.exec();

all()

Use all to get the rows returned from a query:

let users = await sql`
  select * from users
`.all<User>();

first()

Use first to get the first row returned from a query:

let user = await sql`
  select * from users
`.first<User>();

exists()

Use exists to quickly check if any rows match a condition:

let runJobs = await sql`
  select *
  from jobs
  where run_at < now()
  limit 1
`.exists();

paginate()

Use paginate to limit and offset results:

let users = await sql`
  select * from users
`.paginate<User>({ page: 0, per: 100 });

This wraps your query like:

select paginated.* from ( ... ) paginated limit ? offset ?

Composing Queries

Subqueries

Compose queries together with subqueries:

let subQuery = sql`select id from other_table`;

await sql`
  select *
  from table_name
  where other_id in (${subQuery})
`.all();

Query Builders

await sql`
  insert into users ${sql.values({ name: "Ryan", active: false })}
`.exec();
// insert into users(name, active) values ($0, $1)
// $0 = "Ryan"
// $1 = false
await sql`
  insert into users ${sql.values([
    { name: "Ryan", active: false },
    { name: "Nolan", active: false },
  ])}
`.exec();
// insert into users(name, active) values ($0, $1), ($2, $3)
// $0 = "Ryan"
// $1 = false
// $2 = "Nolan"
// $3 = false
await sql`
  update users
  ${sql.set({ name: "Ryan", active: true })}
  where id = ${1}
`.exec();
// update users set name = $0, active = $1 where id = $2
// $0 = "Ryan"
// $1 = true
// $2 = 1
await sql`
  select *
  from users
  where ${sql.and({ id: 1, active: true })}
`.all();
// select * from users where (id = $0 and active = $1)
// $0 = 1
// $1 = true
await sql`
  select *
  from users
  where ${sql.or({ id: 1, active: true })}
`.all();
// select * from users where (id = $0 or active = $1)
// $0 = 1
// $1 = true

Arrays

Convert arrays into a comma-separated list:

await sql`
  select *
  from users
  where id in (${sql.array([1, 2, 3])})
`.exec();
// select * from users where id in ($0, $1, $2)
// $0 = 1
// $1 = 2
// $2 = 3

References

Use sql.ref to reference a column or table:

await sql`
  select ${sql.ref("users.id")}
  from users
`.all();

Join

The signature for join is:

join(delimiter: SqlFragment, [first, ...rest]: SqlFragment[]): SqlFragment

So, the delimiter is the first parameter, and the array of fragments is the second parameter:

await sql`
  select ${sql.join(sql`, `, [sql`users.id`, sql`users.name`])}
  from users
`.all();
// select users.id, users.name from users

Literals

Use sql.literal for direct literal insertion:

await sql`
  insert into points (location)
  values (${sql.literal([100, 100])})
`.exec();

Transactions

@synvox/sql supports two transaction helpers:

  1. sql.transaction – Retries on deadlock. Use this for queries that affect only the database.
  2. sql.impureTransaction – Does not retry on deadlock by default. Use this if the transaction has side effects outside of the database.
await sql.transaction(async (trxSql) => {
  // Use trxSql like normal. Commit and rollback are handled for you.
  // If an error is thrown, the transaction will be rolled back.
});

Dedicated Connection

Use sql.connection if you need a dedicated connection (via your PoolClient) for a set of queries:

await sql.connection(async (sql) => {
  // This block uses a dedicated connection.
  await sql`select pg_sleep(1)`.exec();
});
// Connection is automatically released afterward.

Previewing Queries

You can preview a query string by calling preview:

let migration = sql`
  insert into users ${sql.values({ firstName: "Ryan" })}
`.preview();

// => insert into users (first_name) values ('Ryan')

Migrations

If you’ve installed the optional migration helpers (or have your own system), you can run migrations with something like:

import { migrate } from "@synvox/sql/migrate"; // Hypothetical import

// Where directoryName is the path to your migration files:
await migrate(sql, directoryName);

A migration file might look like:

// migrations/001-create-users.ts

export async function up(sql) {
  await sql`
    create table users (
      id serial primary key,
      first_name text not null,
      last_name text not null
    )
  `.exec();
}

Note: Migrations in @synvox/sql do not currently support “down” migrations.


Seeds

Similarly, if you have the optional seed helpers:

import { seed } from "@synvox/sql/seed"; // Hypothetical import

await seed(sql, directoryName);

A seed file might look like:

// seeds/001-insert-default-users.ts

export async function seed(sql) {
  await sql`
    insert into users ${sql.values({ first_name: "Ryan", last_name: "Allred" })}
    on conflict do nothing
  `.exec();
}

Types

If you’re using the optional type generator:

import { types } from "@synvox/sql/types"; // Hypothetical import

await types(sql, fileNameToWriteTypes, ["schema_name"]);

This will generate a file containing interfaces for each table in the schema. If you omit ["schema_name"], it will generate types for all schemas.


Case Transforms

By default:

  • Rows from the database are camelCased.
  • Identifiers passed through the helper functions (like sql.values or sql.set) are snake_cased in the database.

To change this behavior, call connect with a caseMethod option:

const sql = connect(new MyPool(), {
  caseMethod: "none", // or 'snake' | 'camel'
});

Debugging

This project uses the debug library. To see debug logs, run your app with:

DEBUG=sql:* node yourApp.js

You’ll see logs for queries, transaction attempts, and errors.

About

sql is another sql template string library. Bring your own database client.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published