Git Product home page Git Product logo

sqlfx's Introduction

sqlfx

A SQL toolkit for Effect-TS

https://tim-smart.github.io/sqlfx

Basic example

import { pipe } from "effect/Function"
import * as Config from "effect/Config"
import * as Effect from "effect/Effect"
import * as Pg from "@sqlfx/pg"

const PgLive = Pg.makeLayer({
  database: Config.succeed("effect_pg_dev"),
})

const program = Effect.gen(function* (_) {
  const sql = yield* _(Pg.tag)

  const people = yield* _(
    sql<{
      readonly id: number
      readonly name: string
    }>`SELECT id, name FROM people`,
  )

  yield* _(Effect.log(`Got ${people.length} results!`))
})

pipe(program, Effect.provideLayer(PgLive), Effect.runPromise)

INSERT resolver

import { pipe } from "effect/Function"
import * as Effect from "effect/Effect"
import * as Schema from "@effect/schema/Schema"
import * as Pg from "@sqlfx/pg"

class Person extends Schema.class({
  id: Schema.number,
  name: Schema.string,
  createdAt: Schema.DateFromSelf,
  updatedAt: Schema.DateFromSelf,
}) {}

const InsertPersonSchema = pipe(
  Person.schemaStruct(),
  Schema.omit("id", "createdAt", "updatedAt"),
)

export const makePersonService = Effect.gen(function* (_) {
  const sql = yield* _(Pg.tag)

  const insert = sql.resolver(
    "InsertPerson",
    InsertPersonSchema,
    Person.schema(),
    requests =>
      sql`
        INSERT INTO people
        ${sql.insert(requests)}
        RETURNING people.*
      `,
  ).execute

  return { insert }
})

SELECT resolver

import * as Effect from "effect/Effect"
import * as Schema from "@effect/schema/Schema"
import * as Pg from "@sqlfx/pg"

class Person extends Schema.Class({
  id: Schema.number,
  name: Schema.string,
  createdAt: Schema.DateFromSelf,
  updatedAt: Schema.DateFromSelf,
}) {}

export const makePersonService = Effect.gen(function* (_) {
  const sql = yield* _(Pg.tag)

  const getByIdResolver = sql.idResolver(
    "GetPersonById",
    Schema.number,
    Person.schema(),
    _ => _.id,
    ids => sql`SELECT * FROM people WHERE id IN ${sql(ids)}`,
  )

  const getById = (id: number) =>
    Effect.withRequestCaching("on")(getByIdResolver.execute(id))

  return { getById }
})

Building queries

Safe interpolation

import * as Effect from "effect/Effect"
import * as Pg from "@sqlfx/pg"

export const make = (limit: number) =>
  Effect.gen(function* (_) {
    const sql = yield* _(Pg.tag)

    const statement = sql`SELECT * FROM people LIMIT ${limit}`
    // e.g. SELECT * FROM people LIMIT ?
  })

Unsafe interpolation

import * as Effect from "effect/Effect"
import * as Pg from "@sqlfx/pg"

type OrderBy = "id" | "created_at" | "updated_at"
type SortOrder = "ASC" | "DESC"

export const make = (orderBy: OrderBy, sortOrder: SortOrder) =>
  Effect.gen(function* (_) {
    const sql = yield* _(Pg.tag)

    const statement = sql`SELECT * FROM people ORDER BY ${sql(orderBy)} ${sql.unsafe(sortOrder)}`
    // e.g. SELECT * FROM people ORDER BY `id` ASC
  })

Where clause combinators

AND

import * as Effect from "effect/Effect"
import * as Pg from "@sqlfx/pg"

export const make = (names: string[], cursor: string) =>
  Effect.gen(function* (_) {
    const sql = yield* _(Pg.tag)

    const statement = sql`SELECT * FROM people WHERE ${sql.and([
      sql`name IN ${sql(names)}`,
      sql`created_at < ${sql(cursor)}`,
    ])}`
    // SELECT * FROM people WHERE (name IN ? AND created_at < ?)
  })

OR

import * as Effect from "effect/Effect"
import * as Pg from "@sqlfx/pg"

export const make = (names: string[], cursor: Date) =>
  Effect.gen(function* (_) {
    const sql = yield* _(Pg.tag)

    const statement = sql`SELECT * FROM people WHERE ${sql.or([
      sql`name IN ${sql(names)}`,
      sql`created_at < ${sql(cursor)}`,
    ])}`
    // SELECT * FROM people WHERE (name IN ? OR created_at < ?)
  })

Mixed

import * as Effect from "effect/Effect"
import * as Pg from "@sqlfx/pg"

export const make = (names: string[], afterCursor: Date, beforeCursor: Date) =>
  Effect.gen(function* (_) {
    const sql = yield* _(Pg.tag)

    const statement = sql`SELECT * FROM people WHERE ${sql.or([
      sql`name IN ${sql(names)}`,
      sql.and([
        `created_at >${sql(afterCursor)}`,
        `created_at < ${sql(beforeCursor)}`,
      ]),
    ])}`
    // SELECT * FROM people WHERE (name IN ? OR (created_at > ? AND created_at < ?))
  })

Migrations

A Migrator module is provided, for running migrations.

Migrations are forward-only, and are written in Typescript as Effect's.

Here is an example migration:

// src/migrations/0001_add_users.ts

import * as Effect from "effect/Effect"
import * as Pg from "@sqlfx/pg"

export default Effect.flatMap(
  Pg.tag,
  sql => sql`
    CREATE TABLE users (
      id serial PRIMARY KEY,
      name varchar(255) NOT NULL,
      created_at TIMESTAMP NOT NULL DEFAULT NOW(),
      updated_at TIMESTAMP NOT NULL DEFAULT NOW()
    )
  `,
)

To run your migrations:

// src/main.ts

import * as Effect from "effect/Effect"
import * as Pg from "@sqlfx/pg"
import * as Migrator from "@sqlfx/pg/Migrator"
import * as Config from "effect/Config"
import { fileURLToPath } from "node:url"
import * as Layer from "effect/Layer"
import { pipe } from "effect/Function"

const program = Effect.gen(function* (_) {
  // ...
})

const PgLive = Pg.makeLayer({
  database: Config.succeed("example_database"),
})

const MigratorLive = Layer.provide(
  Migrator.makeLayer({
    directory: fileURLToPath(new URL("migrations", import.meta.url)),
    // Where to put the `_schema.sql` file
    schemaDirectory: "src/migrations",
  }),
  PgLive,
)

const EnvLive = Layer.mergeAll(PgLive, MigratorLive)

pipe(
  program,
  Effect.provideLayer(EnvLive),
  Effect.tapErrorCause(Effect.logErrorCause),
  Effect.runFork,
)

sqlfx's People

Contributors

arekmaz avatar datner avatar fubhy avatar github-actions[bot] avatar jamiehodge avatar jmatsushita avatar mattiamanzati avatar tiborkr avatar tim-smart avatar vecerek avatar wmaurer avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

sqlfx's Issues

Migrator assumes running as a ESModule

The migrator assumes that it can use import here: https://github.com/tim-smart/sqlfx/blob/main/packages/sql/src/Migrator/Node.ts#L32
This makes the migrator fail in projects that haven't migrated over to ESM yet.

This also breaks on Windows where the following code breaks loading:

const MigratorLive = Layer.provide(
	PgLive,
	Migrator.makeLayer({
		loader: Migrator.fromDisk(fileURLToPath(new URL('migrations', import.meta.url))),
		schemaDirectory: 'src/migrations'
	})
);

which fails with:

node:internal/process/promises:288
            triggerUncaughtException(err, true /* fromPromise */);
            ^

[Error: {"reason":"import-error","message":"Could not import migration \"1_init\"\n\nError [ERR_UNSUPPORTED_ESM_URL_SCHEME]: Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. On Windows, absolute paths must be valid file:// URLs. Received protocol 'd:'","_tag":"MigrationError"}] {
  toJSON: [Function (anonymous)],
  toString: [Function (anonymous)],
  [Symbol(effect/Runtime/FiberFailure)]: Symbol(effect/Runtime/FiberFailure),
  [Symbol(effect/Runtime/FiberFailure/Cause)]: {
    _tag: 'Parallel',
    left: { _tag: 'Empty' },
    right: {
      _tag: 'Fail',
      error: {
        reason: 'import-error',
        message: 'Could not import migration "1_init"\n' +
          '\n' +
          "Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. On Windows, absolute paths must be valid file:// URLs. Received protocol 'd:'",
        _tag: 'MigrationError'
      }
    }
  },
  [Symbol(nodejs.util.inspect.custom)]: [Function (anonymous)]
}

Related: nodejs/node#31710 nodejs/node#34765

Oracle support

Oracle has free docker containers and now a js only client as well :D would be nice to have some oracle driver support.

Passing buffer values to `sql` produces invalid queries

Given:

const Args = Schema.struct({
  accountId: Schema.number,
  // ULIDFromBuffer: Schema.Schema<Buffer, ULID>
  ids: Schema.array(ULIDFromBuffer),
});

export type Args = Schema.To<typeof Args>;

export const query = (
  args: Args
) =>
  pipe(
    Sql.tag,
    Effect.bindTo("sql"),
    Effect.bind("input", () => Schema.encode(Args)(args)),
    Effect.map(
      ({ input, sql }) =>
        sql`SELECT * FROM my_events WHERE account_id = ${
          input.accountId
        } AND aggregate_id IN ${sql(input.ids)}`
    )
  );

I would expect the produced SQL query to be something of the following shape:

SELECT * FROM my_events WHERE account_id = ? AND aggregate_id IN (?)

However, in reality, we're getting:

SELECT * FROM my_events WHERE account_id = ? AND aggregate_id IN (`0`,`1`,`2`,`3`,`4`,`5`,`6`,`7`,`8`,`9`,`10`,`11`,`12`,`13`,`14`,`15`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)

A ulid, when encoded as a binary, is 16 bytes long (explains 0...15).

For more details, see Discord: https://discord.com/channels/795981131316985866/1148264145314127913

Migration error with node ESM module

I get this error in a Node project that compiles to ESM modules

Exited failure state: Error: {"reason":"import-error","message":"Could not import migration \"1_create_people\"\n\nError [ERR_REQUIRE_ESM]: require() of ES Module /Users/corentin/Dev/temp/use-relative-time/dist/src/migrations/0001_create_people.js from /Users/corentin/Dev/temp/use-relative-time/node_modules/.pnpm/@[email protected]_@[email protected][email protected]/node_modules/@sqlfx/sql/Migrator/Node/dist/sqlfx-sql-Migrator-Node.cjs.dev.js not supported.\nInstead change the require of 0001_create_people.js in /Users/corentin/Dev/temp/use-relative-time/node_modules/.pnpm/@[email protected]_@[email protected][email protected]/node_modules/@sqlfx/sql/Migrator/Node/dist/sqlfx-sql-Migrator-Node.cjs.dev.js to a dynamic import() which is available in all CommonJS modules.","_tag":"MigrationError"}

I made a reproduction repo available at https://github.com/tatchi/sqlx-repro

pnpm i
pnpm run build
node dist/src/main.js

It works if we compile to CommonJS. You can test with common branch

git checkout common
pnpm run build
node dist/src/main.js

Add sql query logger

It would be useful to have a logger (aspect) that logs the sql query string sent to the db. Especially for being able to track how requests are being batched or when using sqlfx with kysely.

Auto-instrument queries

Following the example of @effect/platform's HTTP Server, sqlfx should also create a span for every sql query, including transaction start, commit, and rollback.

The span should contain the following information:

  • sql statement
  • error (if present)
  • db host (since multiple clients may be used in an application)

It should be made possible to add custom tags. Bonus points, if the above information can be assigned to user-defined tags, e.g. assigning the db host to db.host tag instead of a potential library default host tag.

SELECT WHERE ID in with duplicated IDs

Given the following SQLite table:

CREATE TABLE "users" ("id" integer NOT NULL,"name" varchar NOT NULL, PRIMARY KEY (id));

With two users:

id name
1 user 1
2 user 2

If we manually run the following query:

SELECT * FROM users where id in (1,2,1)

It returns two rows. If we do the same with sqlfx:

const GetById = sql.resolverId('GetUserById', {
  id: S.number,
  result: UserDbo,
  resultId: (_) => _.id,
  run: (ids) => sql`SELECT * FROM users WHERE id IN ${sql(ids)}`,
});

const getUserById = (id: number) =>
  Effect.gen(function* (_) {
    const user = yield* _(
      GetById.execute(id).pipe(Effect.map(Option.map((p) => new UserDbo(p))))
    );

    return user;
  });

const main = Effect.gen(function* (_) {
const { getUserById } = yield* _(UserRepro);

const allUsers = yield* _(
  Effect.all(
    pipe(
      [1, 2, 1],
      ReadonlyArray.map((id) => getUserById(id))
    ),
    { batching: false }
  )
);

console.log(allUsers);

return null;
});		

It returns 3 rows:

[
  {
    _id: 'Option',
    _tag: 'Some',
    value: UserDbo { id: 1, name: 'user 1' }
  },
  {
    _id: 'Option',
    _tag: 'Some',
    value: UserDbo { id: 2, name: 'user 2' }
  },
  {
    _id: 'Option',
    _tag: 'Some',
    value: UserDbo { id: 1, name: 'user 1' }
  }
]

If we set batching to true, it gives the following error

Exited failure state: All fibers interrupted without errors.

Is it expected? Shouldn't the code return 2 rows in both cases?

Repro

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.