@rc312/effect-snowflake 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +174 -2
  4. package/dist/adbc-driver.d.ts +49 -0
  5. package/dist/adbc-driver.d.ts.map +1 -0
  6. package/dist/adbc-driver.js +29 -0
  7. package/dist/adbc-driver.js.map +1 -0
  8. package/dist/index.d.ts +96 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +89 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/internal/client.d.ts +6 -0
  13. package/dist/internal/client.d.ts.map +1 -0
  14. package/dist/internal/client.js +29 -0
  15. package/dist/internal/client.js.map +1 -0
  16. package/dist/internal/configuration.d.ts +4 -0
  17. package/dist/internal/configuration.d.ts.map +1 -0
  18. package/dist/internal/configuration.js +51 -0
  19. package/dist/internal/configuration.js.map +1 -0
  20. package/dist/internal/connection.d.ts +25 -0
  21. package/dist/internal/connection.d.ts.map +1 -0
  22. package/dist/internal/connection.js +39 -0
  23. package/dist/internal/connection.js.map +1 -0
  24. package/dist/internal/lifecycle.d.ts +9 -0
  25. package/dist/internal/lifecycle.d.ts.map +1 -0
  26. package/dist/internal/lifecycle.js +81 -0
  27. package/dist/internal/lifecycle.js.map +1 -0
  28. package/dist/internal/native.d.ts +4 -0
  29. package/dist/internal/native.d.ts.map +1 -0
  30. package/dist/internal/native.js +5 -0
  31. package/dist/internal/native.js.map +1 -0
  32. package/dist/internal/session.d.ts +6 -0
  33. package/dist/internal/session.d.ts.map +1 -0
  34. package/dist/internal/session.js +46 -0
  35. package/dist/internal/session.js.map +1 -0
  36. package/dist/internal/transaction.d.ts +6 -0
  37. package/dist/internal/transaction.d.ts.map +1 -0
  38. package/dist/internal/transaction.js +38 -0
  39. package/dist/internal/transaction.js.map +1 -0
  40. package/dist/models.d.ts +99 -0
  41. package/dist/models.d.ts.map +1 -0
  42. package/dist/models.js +2 -0
  43. package/dist/models.js.map +1 -0
  44. package/dist/snowflake-error.d.ts +19 -0
  45. package/dist/snowflake-error.d.ts.map +1 -0
  46. package/dist/snowflake-error.js +13 -0
  47. package/dist/snowflake-error.js.map +1 -0
  48. package/examples/.env.example +16 -0
  49. package/examples/README.md +73 -0
  50. package/examples/check.ts +12 -0
  51. package/examples/composition.ts +24 -0
  52. package/examples/options.ts +80 -0
  53. package/examples/query.ts +4 -0
  54. package/examples/run.ts +30 -0
  55. package/examples/scenarios.ts +114 -0
  56. package/examples/stream.ts +4 -0
  57. package/examples/transaction.ts +4 -0
  58. package/package.json +59 -4
  59. package/src/adbc-driver.ts +83 -0
  60. package/src/index.ts +151 -0
  61. package/src/internal/client.ts +69 -0
  62. package/src/internal/configuration.ts +63 -0
  63. package/src/internal/connection.ts +83 -0
  64. package/src/internal/lifecycle.ts +186 -0
  65. package/src/internal/native.ts +9 -0
  66. package/src/internal/session.ts +111 -0
  67. package/src/internal/transaction.ts +88 -0
  68. package/src/models.ts +126 -0
  69. package/src/snowflake-error.ts +17 -0
@@ -0,0 +1,24 @@
1
+ import * as Snowflake from "@rc312/effect-snowflake";
2
+ import { tableFromArrays } from "apache-arrow";
3
+ import { Effect, flow, Schema } from "effect";
4
+
5
+ import { runProgram } from "./run.ts";
6
+
7
+ // Build a reusable workflow, add decoding, then wrap the whole Effect in a
8
+ // transaction. No connection or callback needs to be threaded through its steps.
9
+ const lookup = flow(
10
+ (id: bigint) =>
11
+ Snowflake.query(
12
+ 'select ?::number(38, 0)::varchar as "id"',
13
+ tableFromArrays({ id: new BigInt64Array([id]) })
14
+ ),
15
+ Effect.flatMap((table) =>
16
+ Schema.decodeUnknownEffect(Schema.Struct({ id: Schema.String }))(
17
+ table.get(0)?.toJSON()
18
+ )
19
+ ),
20
+ Effect.map((row) => row.id),
21
+ Snowflake.withTransaction
22
+ );
23
+
24
+ await runProgram(lookup(9_007_199_254_740_993n));
@@ -0,0 +1,80 @@
1
+ import type { SnowflakeOptions } from "@rc312/effect-snowflake";
2
+ import { Config, Effect, Option, Redacted } from "effect";
3
+
4
+ /** Shared environment recipe for the runnable examples and live E2E suite. */
5
+ export const optionsFromEnv = Effect.fn("Snowflake.Example.options")(
6
+ function* loadOptions() {
7
+ const settings = yield* Config.all({
8
+ account: Config.NonEmptyString("SNOWFLAKE_ACCOUNT"),
9
+ authenticator: Config.Literals(
10
+ [
11
+ "auth_snowflake",
12
+ "auth_jwt",
13
+ "auth_oauth",
14
+ "auth_ext_browser",
15
+ "auth_okta",
16
+ "auth_mfa",
17
+ "auth_pat",
18
+ "auth_wif",
19
+ ],
20
+ "SNOWFLAKE_AUTHENTICATOR"
21
+ ).pipe(Config.withDefault("auth_jwt")),
22
+ database: Config.NonEmptyString("SNOWFLAKE_DATABASE"),
23
+ driver: Config.NonEmptyString("SNOWFLAKE_ADBC_DRIVER").pipe(
24
+ Config.withDefault("snowflake")
25
+ ),
26
+ schema: Config.NonEmptyString("SNOWFLAKE_SCHEMA"),
27
+ username: Config.NonEmptyString("SNOWFLAKE_USERNAME"),
28
+ warehouse: Config.NonEmptyString("SNOWFLAKE_WAREHOUSE"),
29
+ });
30
+
31
+ const optional = yield* Config.all({
32
+ password: Config.option(Config.Redacted("SNOWFLAKE_PASSWORD")),
33
+ privateKey: Config.option(Config.Redacted("SNOWFLAKE_PRIVATE_KEY")),
34
+ privateKeyPass: Config.option(
35
+ Config.Redacted("SNOWFLAKE_PRIVATE_KEY_PASS")
36
+ ),
37
+ privateKeyPath: Config.option(
38
+ Config.NonEmptyString("SNOWFLAKE_PRIVATE_KEY_PATH")
39
+ ),
40
+ role: Config.option(Config.NonEmptyString("SNOWFLAKE_ROLE")),
41
+ token: Config.option(Config.Redacted("SNOWFLAKE_TOKEN")),
42
+ });
43
+
44
+ const options: SnowflakeOptions = {
45
+ ...settings,
46
+ databaseOptions: {
47
+ "adbc.snowflake.sql.client_option.client_timeout": Redacted.make("30s"),
48
+ "adbc.snowflake.sql.client_option.login_timeout": Redacted.make("30s"),
49
+ "adbc.snowflake.sql.client_option.request_timeout":
50
+ Redacted.make("60s"),
51
+ },
52
+ };
53
+
54
+ if (Option.isSome(optional.password)) {
55
+ Object.assign(options, { password: optional.password.value });
56
+ }
57
+
58
+ if (Option.isSome(optional.privateKey)) {
59
+ Object.assign(options, { privateKey: optional.privateKey.value });
60
+ }
61
+
62
+ if (Option.isSome(optional.privateKeyPass)) {
63
+ Object.assign(options, { privateKeyPass: optional.privateKeyPass.value });
64
+ }
65
+
66
+ if (Option.isSome(optional.privateKeyPath)) {
67
+ Object.assign(options, { privateKeyPath: optional.privateKeyPath.value });
68
+ }
69
+
70
+ if (Option.isSome(optional.role)) {
71
+ Object.assign(options, { role: optional.role.value });
72
+ }
73
+
74
+ if (Option.isSome(optional.token)) {
75
+ Object.assign(options, { token: optional.token.value });
76
+ }
77
+
78
+ return options;
79
+ }
80
+ );
@@ -0,0 +1,4 @@
1
+ import { runExample } from "./run.ts";
2
+ import { queryExample } from "./scenarios.ts";
3
+
4
+ await runExample(queryExample);
@@ -0,0 +1,30 @@
1
+ import { layerFrom, makeWithDriver, Snowflake } from "@rc312/effect-snowflake";
2
+ import type { SnowflakeClient } from "@rc312/effect-snowflake";
3
+ import { layer as nativeDriver } from "@rc312/effect-snowflake/adbc-driver";
4
+ import { Effect, Layer, pipe } from "effect";
5
+
6
+ import { optionsFromEnv } from "./options.ts";
7
+
8
+ // The application chooses configuration and the native loader once. The Layer
9
+ // owns the connection until the entire program, including finalizers, finishes.
10
+ const live = layerFrom(
11
+ optionsFromEnv().pipe(Effect.flatMap(makeWithDriver))
12
+ ).pipe(Layer.provide(nativeDriver));
13
+
14
+ /** Runtime boundary for service-based example programs. */
15
+ export const runProgram = async <A, E>(
16
+ program: Effect.Effect<A, E, Snowflake>
17
+ ) => {
18
+ try {
19
+ const result = await Effect.runPromise(program.pipe(Effect.provide(live)));
20
+ console.dir(result, { depth: null });
21
+ } catch (error) {
22
+ console.error(error);
23
+ process.exitCode = 1;
24
+ }
25
+ };
26
+
27
+ /** The scoped client factory API composes with the same application layer. */
28
+ export const runExample = <A, E>(
29
+ use: (client: SnowflakeClient) => Effect.Effect<A, E>
30
+ ) => runProgram(pipe(Snowflake, Effect.flatMap(use)));
@@ -0,0 +1,114 @@
1
+ import type { SnowflakeClient } from "@rc312/effect-snowflake";
2
+ import {
3
+ Bool,
4
+ Int64,
5
+ Table,
6
+ tableFromArrays,
7
+ Utf8,
8
+ vectorFromArray,
9
+ } from "apache-arrow";
10
+ import { Data, Effect, Schema, Stream } from "effect";
11
+
12
+ const queryRow = Schema.Struct({
13
+ enabled: Schema.Boolean,
14
+ id: Schema.String,
15
+ label: Schema.String,
16
+ optional: Schema.Null,
17
+ });
18
+
19
+ /** Bind Arrow int64, text, boolean, and a typed null, then decode a result. */
20
+ export const queryExample = Effect.fn("Snowflake.Example.query")(
21
+ function* query(client: SnowflakeClient) {
22
+ const parameters = new Table({
23
+ enabled: vectorFromArray([true], new Bool()),
24
+ id: vectorFromArray([9_007_199_254_740_993n], new Int64()),
25
+ label: vectorFromArray(["hello ? -- Snowflake"], new Utf8()),
26
+ optional: vectorFromArray([null], new Utf8()),
27
+ });
28
+
29
+ const result = yield* client.query(
30
+ 'select ?::boolean as "enabled", ?::number(38, 0)::varchar as "id", ?::varchar as "label", ?::varchar as "optional"',
31
+ parameters
32
+ );
33
+
34
+ return yield* Schema.decodeUnknownEffect(queryRow)(result.get(0)?.toJSON());
35
+ }
36
+ );
37
+
38
+ /** Count a complete stream, stop another after one batch, then reuse its client. */
39
+ export const streamExample = Effect.fn("Snowflake.Example.stream")(
40
+ function* stream(client: SnowflakeClient) {
41
+ const totalRows = yield* client
42
+ .stream("select seq4() from table(generator(rowcount => 1000))")
43
+ .pipe(
44
+ Stream.runFold(
45
+ () => 0,
46
+ (count, batch) => count + batch.numRows
47
+ )
48
+ );
49
+
50
+ const firstBatchRows = yield* client
51
+ .stream("select seq4() from table(generator(rowcount => 100000))")
52
+ .pipe(
53
+ Stream.filter((batch) => batch.numRows > 0),
54
+ Stream.take(1),
55
+ Stream.runFold(
56
+ () => 0,
57
+ (count, batch) => count + batch.numRows
58
+ )
59
+ );
60
+
61
+ const next = yield* client.query("select 'ok' as \"status\"");
62
+
63
+ const { status } = yield* Schema.decodeUnknownEffect(
64
+ Schema.Struct({ status: Schema.Literal("ok") })
65
+ )(next.get(0)?.toJSON());
66
+
67
+ return { firstBatchRows, status, totalRows };
68
+ }
69
+ );
70
+
71
+ class RollbackExample extends Data.TaggedError("RollbackExample") {}
72
+
73
+ /** Create a session-temporary table before DML transactions and always drop it. */
74
+ export const transactionExample = Effect.fn("Snowflake.Example.transaction")(
75
+ function* transactions(client: SnowflakeClient) {
76
+ return yield* Effect.acquireUseRelease(
77
+ client.execute(
78
+ "create temporary table EFFECT_SNOWFLAKE_EXAMPLE (ID number(38, 0))"
79
+ ),
80
+ () =>
81
+ Effect.gen(function* commitAndRollback() {
82
+ yield* client
83
+ .execute(
84
+ "insert into EFFECT_SNOWFLAKE_EXAMPLE values (?)",
85
+ tableFromArrays({ id: [1] })
86
+ )
87
+ .pipe(client.withTransaction);
88
+ yield* client
89
+ .execute(
90
+ "insert into EFFECT_SNOWFLAKE_EXAMPLE values (?)",
91
+ tableFromArrays({ id: [2] })
92
+ )
93
+ .pipe(
94
+ Effect.andThen(Effect.fail(new RollbackExample())),
95
+ client.withTransaction,
96
+ Effect.catchTag("RollbackExample", () => Effect.void)
97
+ );
98
+
99
+ const rows = yield* client.query(
100
+ 'select ID::varchar as "id" from EFFECT_SNOWFLAKE_EXAMPLE order by ID'
101
+ );
102
+
103
+ return yield* Schema.decodeEffect(
104
+ Schema.Array(Schema.Struct({ id: Schema.String }))
105
+ )(Array.from(rows, (row) => row.toJSON()));
106
+ }),
107
+ // A failed drop must remain visible even when the transaction already failed.
108
+ () =>
109
+ client
110
+ .execute("drop table if exists EFFECT_SNOWFLAKE_EXAMPLE")
111
+ .pipe(Effect.orDie)
112
+ );
113
+ }
114
+ );
@@ -0,0 +1,4 @@
1
+ import { runExample } from "./run.ts";
2
+ import { streamExample } from "./scenarios.ts";
3
+
4
+ await runExample(streamExample);
@@ -0,0 +1,4 @@
1
+ import { runExample } from "./run.ts";
2
+ import { transactionExample } from "./scenarios.ts";
3
+
4
+ await runExample(transactionExample);
package/package.json CHANGED
@@ -1,6 +1,61 @@
1
1
  {
2
2
  "name": "@rc312/effect-snowflake",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Scoped Effect 4 Snowflake client with Apache Arrow ADBC queries, streams, and transactions.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/RyanCallahan312/effect-extensions.git",
9
+ "directory": "packages/effect-snowflake"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "src",
14
+ "README.md",
15
+ "CHANGELOG.md",
16
+ "examples"
17
+ ],
18
+ "type": "module",
19
+ "sideEffects": false,
20
+ "exports": {
21
+ ".": "./dist/index.js",
22
+ "./package.json": "./package.json",
23
+ "./adbc-driver": "./dist/adbc-driver.js"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public",
27
+ "registry": "https://registry.npmjs.org/"
28
+ },
29
+ "scripts": {
30
+ "build": "tsc -b tsconfig.json",
31
+ "dev": "tsc -b tsconfig.json --watch",
32
+ "typecheck": "bun run build && tsc --noEmit -p tsconfig.json --tsBuildInfoFile .cache/typecheck.tsbuildinfo && tsc -p tsconfig.test.json && tsc -p tsconfig.examples.json --noEmit",
33
+ "test": "bun run build && vitest run && bun run test:cli",
34
+ "prepack": "bun run build",
35
+ "test:coverage": "bun run build && vitest run --coverage",
36
+ "build:examples": "bun run build && tsc -p tsconfig.examples.json",
37
+ "example:query": "bun run build:examples && node --env-file-if-exists=.env .cache/examples/examples/query.js",
38
+ "example:stream": "bun run build:examples && node --env-file-if-exists=.env .cache/examples/examples/stream.js",
39
+ "example:transaction": "bun run build:examples && node --env-file-if-exists=.env .cache/examples/examples/transaction.js",
40
+ "example:check": "bun run build:examples && node --env-file-if-exists=.env .cache/examples/examples/check.js",
41
+ "test:e2e": "bun run build:examples && node --env-file-if-exists=.env --test .cache/examples/e2e/*.test.js",
42
+ "test:cli": "bun run build:examples && node --test test/*.test.mjs",
43
+ "example:composition": "bun run build:examples && node --env-file-if-exists=.env .cache/examples/examples/composition.js"
44
+ },
45
+ "dependencies": {
46
+ "@apache-arrow/adbc-driver-manager": "~0.24.0"
47
+ },
48
+ "devDependencies": {
49
+ "@effect/vitest": "^4.1.2",
50
+ "@types/node": "^26.6.5",
51
+ "@vitest/coverage-v8": "5.0.4",
52
+ "vitest": "^5.0.4"
53
+ },
54
+ "peerDependencies": {
55
+ "apache-arrow": "^21.1.0",
56
+ "effect": "^4.1.2"
57
+ },
58
+ "engines": {
59
+ "node": ">=22.19.0"
60
+ }
61
+ }
@@ -0,0 +1,83 @@
1
+ import type {
2
+ AdbcConnection,
3
+ AdbcStatement,
4
+ AdbcDatabase,
5
+ ConnectOptions,
6
+ } from "@apache-arrow/adbc-driver-manager";
7
+ import { Context, Effect, Layer } from "effect";
8
+
9
+ import { failure } from "./snowflake-error.ts";
10
+ import type { SnowflakeError } from "./snowflake-error.ts";
11
+
12
+ /**
13
+ * Minimal native database resource owned by a scoped client.
14
+ * Implementations must await acquisition/cleanup rather than detach native work.
15
+ * @since 0.1.0
16
+ */
17
+ export interface Database {
18
+ /** Open a connection; the adapter owns it after this Promise resolves. */
19
+ readonly connect: () => Promise<Connection>;
20
+ /** Release database resources after all owned connections have closed. */
21
+ readonly close: AdbcDatabase["close"];
22
+ }
23
+
24
+ /**
25
+ * Native statement operations used by the adapter. Each query receives a fresh
26
+ * instance. A returned reader is released before `close` is invoked.
27
+ * @since 0.1.0
28
+ */
29
+ export type Statement = Pick<
30
+ AdbcStatement,
31
+ "setSqlQuery" | "bind" | "executeQuery" | "executeUpdate" | "close"
32
+ >;
33
+
34
+ /**
35
+ * Native connection boundary. Methods are called under an exclusive permit;
36
+ * `createStatement` must return a fresh statement for each call.
37
+ * @since 0.1.0
38
+ */
39
+ export type Connection = Pick<
40
+ AdbcConnection,
41
+ "setAutoCommit" | "commit" | "rollback" | "close"
42
+ > & { readonly createStatement: () => Promise<Statement> };
43
+
44
+ /**
45
+ * Loader service required by `makeWithDriver` for alternate native loading or
46
+ * controlled tests. `open` must map native failures to sanitized Snowflake errors.
47
+ * Use {@link layer} for the standard Apache Arrow implementation.
48
+ * @since 0.1.0
49
+ */
50
+ export class AdbcDriver extends Context.Service<
51
+ AdbcDriver,
52
+ {
53
+ /** Construct the database; the client owns and closes a successful result. */
54
+ readonly open: (
55
+ options: ConnectOptions
56
+ ) => Effect.Effect<Database, SnowflakeError>;
57
+ }
58
+ >()("@rc312/effect-snowflake/AdbcDriver") {}
59
+
60
+ /**
61
+ * Standard loader layer. Importing it does not load the native addon; `open` does.
62
+ * Addon import failures use operation `driver.load`; database construction failures
63
+ * use `database.open`. Raw native messages and causes are excluded from both.
64
+ * @since 0.1.0
65
+ */
66
+ export const layer = Layer.succeed(
67
+ AdbcDriver,
68
+ AdbcDriver.of({
69
+ open: Effect.fn("Snowflake.AdbcDriver.open")(
70
+ function* openDatabase(options) {
71
+ const { AdbcDatabase } = yield* Effect.tryPromise({
72
+ catch: failure("driver.load"),
73
+ try: () => import("@apache-arrow/adbc-driver-manager"),
74
+ });
75
+
76
+ return yield* Effect.try({
77
+ catch: failure("database.open"),
78
+ try: () => new AdbcDatabase(options),
79
+ });
80
+ }
81
+ ),
82
+ })
83
+ );
package/src/index.ts ADDED
@@ -0,0 +1,151 @@
1
+ import type { RecordBatch, Table } from "apache-arrow";
2
+ import { Config, Context, Effect, flow, pipe, Layer, Stream } from "effect";
3
+
4
+ import { layer as driverLayer } from "./adbc-driver.ts";
5
+ import * as Client from "./internal/client.ts";
6
+ import * as Connection from "./internal/connection.ts";
7
+ import { acquireOwner } from "./internal/lifecycle.ts";
8
+ import type {
9
+ SnowflakeClient,
10
+ SnowflakeOptions,
11
+ SnowflakeSession,
12
+ } from "./models.ts";
13
+ import type { SnowflakeError } from "./snowflake-error.ts";
14
+
15
+ export type {
16
+ SnowflakeClient,
17
+ SnowflakeOptions,
18
+ SnowflakeSession,
19
+ } from "./models.ts";
20
+
21
+ export { SnowflakeError } from "./snowflake-error.ts";
22
+
23
+ /**
24
+ * Injected client service. Its layer owns one connection and captures its driver.
25
+ * Reuse the same layer value when repositories should share that connection.
26
+ * @since 0.1.0
27
+ */
28
+ export class Snowflake extends Context.Service<
29
+ Snowflake,
30
+ {
31
+ readonly query: SnowflakeSession["query"];
32
+ readonly execute: SnowflakeSession["execute"];
33
+ readonly stream: SnowflakeSession["stream"];
34
+ readonly transaction: SnowflakeClient["transaction"];
35
+ readonly withTransaction: SnowflakeClient["withTransaction"];
36
+ }
37
+ >()("@rc312/effect-snowflake/Snowflake") {}
38
+
39
+ /**
40
+ * Acquire a connected client through an injected `AdbcDriver` service.
41
+ *
42
+ * @param options Native Snowflake settings; construction performs no I/O.
43
+ * @returns An Effect requiring `Scope` and `AdbcDriver`. The scope joins active
44
+ * operations before closing connection and database, even under parallel teardown.
45
+ * Failed acquisition immediately releases its partial resources and child scope.
46
+ *
47
+ * Custom loaders must normalize native errors to sanitized `SnowflakeError`s.
48
+ * Native calls have no cancellation API: configure driver network/login timeouts
49
+ * to bound shutdown. Prefer {@link make} when using the standard native manager.
50
+ * @since 0.1.0
51
+ */
52
+ export const makeWithDriver = Effect.fn("Snowflake.make")(
53
+ flow(Connection.make, Effect.flatMap(Client.make), acquireOwner)
54
+ );
55
+
56
+ /**
57
+ * Acquire a server-only Snowflake connection through the Apache Arrow ADBC manager.
58
+ *
59
+ * @param options Connection settings. The Snowflake shared-library driver must be
60
+ * installed separately; `driver` defaults to `snowflake`.
61
+ * @returns A lazy Effect requiring `Scope`. Its scope owns connection and database.
62
+ * Driver loading and connection errors are sanitized `SnowflakeError`s. Native
63
+ * calls settle before interruption releases resources; queries are never retried.
64
+ *
65
+ * @example
66
+ * ```ts
67
+ * import { make } from "@rc312/effect-snowflake";
68
+ * import { Config, Effect } from "effect";
69
+ *
70
+ * const program = Effect.scoped(Effect.gen(function* () {
71
+ * const db = yield* make({
72
+ * account: yield* Config.String("SNOWFLAKE_ACCOUNT"),
73
+ * username: yield* Config.String("SNOWFLAKE_USERNAME"),
74
+ * password: yield* Config.Redacted("SNOWFLAKE_PASSWORD"),
75
+ * });
76
+ * return yield* db.query("select current_version() as VERSION");
77
+ * }));
78
+ * ```
79
+ * @since 0.1.0
80
+ */
81
+ export const make = Effect.fn("Snowflake.makeDefault")(
82
+ (options: SnowflakeOptions) =>
83
+ makeWithDriver(options).pipe(Effect.provide(driverLayer))
84
+ );
85
+
86
+ /**
87
+ * Provide the service from an existing acquisition Effect, retaining its errors
88
+ * and dependencies. The Layer owns any Scope required by the acquisition.
89
+ * Useful for application-owned configuration, instrumentation, and test clients.
90
+ * @since 0.1.0
91
+ */
92
+ export const layerFrom = <E, R>(
93
+ acquire: Effect.Effect<SnowflakeClient, E, R>
94
+ ) => Layer.effect(Snowflake, acquire);
95
+
96
+ /**
97
+ * Provide a scoped Snowflake service using an injected AdbcDriver.
98
+ * Compose with `Layer.provide(AdbcDriver.layer)` at the application boundary.
99
+ * @since 0.1.0
100
+ */
101
+ export const layer = (options: SnowflakeOptions) =>
102
+ layerFrom(makeWithDriver(options));
103
+
104
+ /**
105
+ * Read Config values when the layer is built, then acquire its scoped client.
106
+ * Accepts one Config or a record of Config fields; ConfigProvider stays caller-owned.
107
+ * Requires the same explicit AdbcDriver dependency as {@link layer}.
108
+ * @since 0.1.0
109
+ */
110
+ export const layerConfig = (options: Config.Wrap<SnowflakeOptions>) =>
111
+ Layer.unwrap(Config.unwrap(options).pipe(Effect.map(layer)));
112
+
113
+ /** Query the injected client, or the active transaction's scoped session. @since 0.1.0 */
114
+ export const query = (sql: string, parameters?: Table) =>
115
+ pipe(
116
+ Snowflake,
117
+ Effect.flatMap((client) => client.query(sql, parameters))
118
+ );
119
+
120
+ /** Execute DML/DDL on the injected client. @since 0.1.0 */
121
+ export const execute = (sql: string, parameters?: Table) =>
122
+ pipe(
123
+ Snowflake,
124
+ Effect.flatMap((client) => client.execute(sql, parameters))
125
+ );
126
+
127
+ /** Lazily stream batches from the injected client; the consumer owns its permit. @since 0.1.0 */
128
+ export const stream = (
129
+ sql: string,
130
+ parameters?: Table
131
+ ): Stream.Stream<RecordBatch, SnowflakeError, Snowflake> =>
132
+ Stream.unwrap(
133
+ pipe(
134
+ Snowflake,
135
+ Effect.map((client) => client.stream(sql, parameters))
136
+ )
137
+ );
138
+
139
+ /**
140
+ * Run an Effect in a transaction using the injected client. Pipe it around a
141
+ * composed workflow; accessors and `yield* Snowflake` both resolve its session.
142
+ * Nested transactions fail with operation `transaction.nested` (Snowflake has no
143
+ * savepoints). Callback errors, defects, service requirements, and interruption
144
+ * are preserved by the underlying transaction implementation.
145
+ * @since 0.1.0
146
+ */
147
+ export const withTransaction = <A, E, R>(self: Effect.Effect<A, E, R>) =>
148
+ pipe(
149
+ Snowflake,
150
+ Effect.flatMap((client) => client.withTransaction(self))
151
+ );
@@ -0,0 +1,69 @@
1
+ import { Context, Effect, Option, Semaphore, Stream } from "effect";
2
+
3
+ import type { SnowflakeClient, SnowflakeSession } from "../models.ts";
4
+ import { failure } from "../snowflake-error.ts";
5
+ import type { Connection } from "./connection.ts";
6
+ import * as Session from "./session.ts";
7
+ import * as Transaction from "./transaction.ts";
8
+
9
+ // Each client needs its own fiber-local session key, as in Effect SqlClient.
10
+ // A transaction on one client must never route another client's queries.
11
+ let nextClientId = 0;
12
+
13
+ /** Capture the connection; client methods have no dependency requirements. */
14
+ export const make = Effect.fnUntraced(function* makeClient(
15
+ connection: Connection
16
+ ): Effect.fn.Return<SnowflakeClient> {
17
+ const lock = yield* Semaphore.make(1);
18
+
19
+ const direct = Session.make(
20
+ connection,
21
+ connection.owner,
22
+ lock,
23
+ connection.isOpen
24
+ );
25
+
26
+ const transactionSession = Context.Service<SnowflakeSession>(
27
+ `@rc312/effect-snowflake/Transaction/${(nextClientId += 1)}`
28
+ );
29
+
30
+ const currentSession = Effect.serviceOption(transactionSession).pipe(
31
+ Effect.map(Option.getOrElse(() => direct))
32
+ );
33
+
34
+ const runTransaction = Transaction.make(connection, lock);
35
+
36
+ const transaction: SnowflakeClient["transaction"] = <A, E, R>(
37
+ use: (session: SnowflakeSession) => Effect.Effect<A, E, R>
38
+ ) =>
39
+ Effect.serviceOption(transactionSession).pipe(
40
+ Effect.flatMap((current) => {
41
+ if (Option.isSome(current)) {
42
+ return Effect.fail(failure("transaction.nested")());
43
+ }
44
+
45
+ return runTransaction((session) =>
46
+ Effect.provideService(use(session), transactionSession, session)
47
+ );
48
+ })
49
+ );
50
+
51
+ return {
52
+ execute: (sql, parameters) =>
53
+ currentSession.pipe(
54
+ Effect.flatMap((session) => session.execute(sql, parameters))
55
+ ),
56
+ query: (sql, parameters) =>
57
+ currentSession.pipe(
58
+ Effect.flatMap((session) => session.query(sql, parameters))
59
+ ),
60
+ stream: (sql, parameters) =>
61
+ Stream.unwrap(
62
+ currentSession.pipe(
63
+ Effect.map((session) => session.stream(sql, parameters))
64
+ )
65
+ ),
66
+ transaction,
67
+ withTransaction: (self) => transaction(() => self),
68
+ };
69
+ });