@rc312/effect-snowflake 0.0.0-stage → 0.1.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 +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +176 -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,63 @@
1
+ import type { ConnectOptions } from "@apache-arrow/adbc-driver-manager";
2
+ import { Redacted } from "effect";
3
+
4
+ import type { SnowflakeOptions } from "../models.ts";
5
+
6
+ export const configuration = (options: SnowflakeOptions): ConnectOptions => {
7
+ const databaseOptions = Object.fromEntries(
8
+ Object.entries(options.databaseOptions ?? {}).map(([key, value]) => [
9
+ key,
10
+ Redacted.value(value),
11
+ ])
12
+ );
13
+
14
+ const values = [
15
+ ["adbc.snowflake.sql.account", options.account],
16
+ ["username", options.username],
17
+ ["adbc.snowflake.sql.db", options.database],
18
+ ["adbc.snowflake.sql.schema", options.schema],
19
+ ["adbc.snowflake.sql.warehouse", options.warehouse],
20
+ ["adbc.snowflake.sql.role", options.role],
21
+ ["adbc.snowflake.sql.auth_type", options.authenticator],
22
+ [
23
+ "adbc.snowflake.sql.client_option.jwt_private_key",
24
+ options.privateKeyPath,
25
+ ],
26
+ ] as const;
27
+
28
+ for (const [key, value] of values) {
29
+ if (value !== undefined) {
30
+ databaseOptions[key] = value;
31
+ }
32
+ }
33
+
34
+ const secrets = [
35
+ ["password", options.password],
36
+ ["adbc.snowflake.sql.client_option.auth_token", options.token],
37
+ [
38
+ "adbc.snowflake.sql.client_option.jwt_private_key_pkcs8_value",
39
+ options.privateKey,
40
+ ],
41
+ [
42
+ "adbc.snowflake.sql.client_option.jwt_private_key_pkcs8_password",
43
+ options.privateKeyPass,
44
+ ],
45
+ ] as const;
46
+
47
+ for (const [key, value] of secrets) {
48
+ if (value !== undefined) {
49
+ databaseOptions[key] = Redacted.value(value);
50
+ }
51
+ }
52
+
53
+ const config: ConnectOptions = {
54
+ databaseOptions,
55
+ driver: options.driver ?? "snowflake",
56
+ };
57
+
58
+ if (options.manifestSearchPaths !== undefined) {
59
+ config.manifestSearchPaths = [...options.manifestSearchPaths];
60
+ }
61
+
62
+ return config;
63
+ };
@@ -0,0 +1,83 @@
1
+ import type { Scope } from "effect";
2
+ import { Effect, flow } from "effect";
3
+
4
+ import { AdbcDriver } from "../adbc-driver.ts";
5
+ import type { Connection as NativeConnection } from "../adbc-driver.ts";
6
+ import type { SnowflakeOptions } from "../models.ts";
7
+ import { failure } from "../snowflake-error.ts";
8
+ import type { SnowflakeError } from "../snowflake-error.ts";
9
+ import { configuration } from "./configuration.ts";
10
+ import { asCleanup } from "./lifecycle.ts";
11
+ import { call } from "./native.ts";
12
+
13
+ /** One native connection and the lifetime shared by all of its sessions. */
14
+ export interface Connection {
15
+ readonly native: NativeConnection;
16
+ readonly owner: Scope.Scope;
17
+ readonly isOpen: () => boolean;
18
+ readonly isHealthy: () => boolean;
19
+ readonly cleanup: <A>(
20
+ operation: string,
21
+ run: () => Promise<A>
22
+ ) => Effect.Effect<A>;
23
+ readonly setAutoCommit: (
24
+ enabled: boolean
25
+ ) => Effect.Effect<void, SnowflakeError>;
26
+ }
27
+
28
+ /** Called inside the client's private sequential scope. Dependencies are captured once. */
29
+ export const make = Effect.fnUntraced(function* acquireConnection(
30
+ options: SnowflakeOptions
31
+ ) {
32
+ const driver = yield* AdbcDriver;
33
+ const owner = yield* Effect.scope;
34
+
35
+ const config = yield* Effect.try({
36
+ catch: failure("options"),
37
+ try: () => configuration(options),
38
+ });
39
+
40
+ let open = true;
41
+ let healthy = true;
42
+
43
+ const invalidate = Effect.sync(() => {
44
+ healthy = false;
45
+ });
46
+
47
+ const finalize = flow(
48
+ Effect.tapError(() => invalidate),
49
+ asCleanup
50
+ );
51
+
52
+ const cleanup: Connection["cleanup"] = (operation, run) =>
53
+ call(operation, run).pipe(finalize);
54
+
55
+ const database = yield* Effect.acquireRelease(
56
+ driver.open(config),
57
+ (resource) => cleanup("database.close", () => resource.close())
58
+ );
59
+
60
+ const native = yield* Effect.acquireRelease(
61
+ call("connect", () => database.connect()),
62
+ (resource) => cleanup("connection.close", () => resource.close())
63
+ );
64
+
65
+ yield* Effect.addFinalizer(() =>
66
+ Effect.sync(() => {
67
+ open = false;
68
+ })
69
+ );
70
+
71
+ return {
72
+ cleanup,
73
+ isHealthy: () => healthy,
74
+ isOpen: () => open && healthy,
75
+ native,
76
+ owner,
77
+ setAutoCommit: (enabled: boolean) =>
78
+ Effect.try({
79
+ catch: failure("transaction.autocommit"),
80
+ try: () => native.setAutoCommit(enabled),
81
+ }).pipe(Effect.tapError(() => invalidate)),
82
+ } satisfies Connection;
83
+ });
@@ -0,0 +1,186 @@
1
+ import type { RecordBatch } from "apache-arrow";
2
+ import { Cause, Effect, Exit, Fiber, Scope, Stream } from "effect";
3
+
4
+ import { failure } from "../snowflake-error.ts";
5
+ import type { SnowflakeError } from "../snowflake-error.ts";
6
+
7
+ const cleanupReason = <E>(reason: Cause.Reason<E>): Cause.Reason<never> =>
8
+ Cause.isFailReason(reason)
9
+ ? Cause.makeDieReason(reason.error).annotate(
10
+ Cause.reasonAnnotations(reason)
11
+ )
12
+ : reason;
13
+
14
+ const cleanupCause = <E>(cause: Cause.Cause<E>) =>
15
+ Cause.fromReasons<never>(cause.reasons.map(cleanupReason));
16
+
17
+ export const asCleanup = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
18
+ Effect.catchCause(effect, (cause) => Effect.failCause(cleanupCause(cause)));
19
+
20
+ const cleanupExit = <A, E>(exit: Exit.Exit<A, E>) => {
21
+ if (Exit.isFailure(exit)) {
22
+ const reasons = exit.cause.reasons.filter(
23
+ (reason) => !Cause.isInterruptReason(reason)
24
+ );
25
+
26
+ if (reasons.length > 0) {
27
+ return Effect.failCause(Cause.fromReasons(reasons)).pipe(asCleanup);
28
+ }
29
+ }
30
+
31
+ return Effect.void;
32
+ };
33
+
34
+ const observeCleanup = <A, E>(fiber: Fiber.Fiber<A, E>) =>
35
+ Fiber.await(fiber).pipe(Effect.flatMap(cleanupExit));
36
+
37
+ const stopOperation = Effect.fnUntraced(function* stopOwnedOperation<A, E>(
38
+ fiber: Fiber.Fiber<A, E>
39
+ ) {
40
+ if (fiber.pollUnsafe() !== undefined) {
41
+ return;
42
+ }
43
+
44
+ yield* Fiber.interrupt(fiber);
45
+ yield* observeCleanup(fiber);
46
+ });
47
+
48
+ interface Operation<A, E> {
49
+ readonly fiber: Fiber.Fiber<A, E>;
50
+ readonly lifetime: Scope.Closeable;
51
+ readonly stop: Effect.Effect<void>;
52
+ }
53
+
54
+ const acquireOperation = Effect.fnUntraced(function* acquireOwnedOperation<
55
+ A,
56
+ E,
57
+ R,
58
+ >(effect: Effect.Effect<A, E, R>, scope: Scope.Scope) {
59
+ const lifetime = yield* Scope.fork(scope, "sequential");
60
+ const fiber = yield* Effect.forkIn(Effect.interruptible(effect), lifetime);
61
+ // forkIn joins on closure but discards the exit; observe cleanup failures too.
62
+ const stop = yield* stopOperation(fiber).pipe(Effect.cached);
63
+ yield* Scope.addFinalizer(lifetime, stop);
64
+
65
+ return { fiber, lifetime, stop } satisfies Operation<A, E>;
66
+ });
67
+
68
+ const releaseOperation = <A, E>(
69
+ { fiber, lifetime, stop }: Operation<A, E>,
70
+ exit: Exit.Exit<A, E>
71
+ ) =>
72
+ stop.pipe(
73
+ Effect.andThen(
74
+ Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)
75
+ ? observeCleanup(fiber)
76
+ : Effect.void
77
+ ),
78
+ Effect.ensuring(Scope.close(lifetime, exit))
79
+ );
80
+
81
+ // Each operation belongs to both its caller and its client/session Scope. Closing
82
+ // either interrupts AND joins it before the ADBC connection can be destroyed.
83
+ export const owned = <A, E, R>(
84
+ effect: Effect.Effect<A, E, R>,
85
+ scope: Scope.Scope,
86
+ isOpen: () => boolean
87
+ ): Effect.Effect<A, E | SnowflakeError, R> =>
88
+ Effect.suspend<A, E | SnowflakeError, R>(() =>
89
+ isOpen()
90
+ ? Effect.acquireUseRelease(
91
+ acquireOperation(effect, scope),
92
+ ({ fiber }) => Fiber.join(fiber),
93
+ releaseOperation
94
+ )
95
+ : Effect.fail(failure("closed")())
96
+ );
97
+
98
+ // Finalizers and concurrent callers must join the same teardown. Scope.close
99
+ // marks a scope closed before its finalizers settle, so it cannot provide this
100
+ // join by itself. Keep the parent registration attached until teardown finishes.
101
+ const makeLifetime = Effect.fnUntraced(function* makeOwnedLifetime(
102
+ owner: Scope.Scope
103
+ ) {
104
+ const registration = yield* Scope.fork(owner, "sequential");
105
+ const scope = yield* Scope.make("sequential");
106
+ // Native resources must finalize after any fiber that can acquire or use them.
107
+ const resources = yield* Scope.fork(scope, "sequential");
108
+ let firstExit: Exit.Exit<unknown, unknown> | undefined;
109
+ let retired = false;
110
+ let open = true;
111
+
112
+ const retire = Effect.suspend(() => {
113
+ retired = true;
114
+
115
+ return Scope.close(registration, firstExit ?? Exit.void);
116
+ });
117
+
118
+ const teardown = yield* Effect.suspend(() => {
119
+ open = false;
120
+
121
+ return Scope.close(scope, firstExit ?? Exit.void);
122
+ }).pipe(Effect.ensuring(retire), Effect.uninterruptible, Effect.cached);
123
+
124
+ const close = (exit: Exit.Exit<unknown, unknown>) =>
125
+ Effect.suspend(() => {
126
+ firstExit ??= exit;
127
+
128
+ return teardown;
129
+ });
130
+
131
+ yield* Scope.addFinalizerExit(registration, (exit) =>
132
+ retired ? Effect.void : close(exit)
133
+ );
134
+
135
+ return { close, isOpen: () => open, resources, scope };
136
+ });
137
+
138
+ const acquireOwnedStream = Effect.fnUntraced(function* acquireStreamLifetime(
139
+ stream: Stream.Stream<RecordBatch, SnowflakeError>,
140
+ owner: Scope.Scope,
141
+ isOpen: () => boolean
142
+ ) {
143
+ if (!isOpen()) {
144
+ return yield* failure("closed")();
145
+ }
146
+
147
+ const lifetime = yield* Effect.acquireRelease(
148
+ makeLifetime(owner),
149
+ (resource, exit) => resource.close(exit)
150
+ );
151
+
152
+ const active = () => lifetime.isOpen() && isOpen();
153
+
154
+ const pull = yield* owned(
155
+ Stream.toPull(stream).pipe(Scope.provide(lifetime.resources)),
156
+ lifetime.scope,
157
+ active
158
+ );
159
+
160
+ return Stream.fromPull(Effect.succeed(owned(pull, lifetime.scope, active)));
161
+ });
162
+
163
+ export const ownedStream = (
164
+ stream: Stream.Stream<RecordBatch, SnowflakeError>,
165
+ owner: Scope.Scope,
166
+ isOpen: () => boolean
167
+ ) => Stream.unwrap(acquireOwnedStream(stream, owner, isOpen));
168
+
169
+ /** Join interrupted acquisition before releasing its late native resources. */
170
+ export const acquireOwner = Effect.fnUntraced(function* acquireClientOwner<
171
+ A,
172
+ E,
173
+ R,
174
+ >(self: Effect.Effect<A, E, R>) {
175
+ return yield* Effect.acquireUseRelease(
176
+ makeLifetime(yield* Effect.scope),
177
+ (lifetime) =>
178
+ owned(
179
+ self.pipe(Scope.provide(lifetime.resources)),
180
+ lifetime.scope,
181
+ lifetime.isOpen
182
+ ),
183
+ (lifetime, exit) =>
184
+ Exit.isFailure(exit) ? lifetime.close(exit) : Effect.void
185
+ );
186
+ });
@@ -0,0 +1,9 @@
1
+ import { Effect } from "effect";
2
+
3
+ import { failure } from "../snowflake-error.ts";
4
+
5
+ /** ADBC JS cannot cancel promises; settle before releasing or reusing resources. */
6
+ export const call = <A>(operation: string, run: () => Promise<A>) =>
7
+ Effect.tryPromise({ catch: failure(operation), try: run }).pipe(
8
+ Effect.uninterruptible
9
+ );
@@ -0,0 +1,111 @@
1
+ import { Table } from "apache-arrow";
2
+ import type { RecordBatch } from "apache-arrow";
3
+ import { Cause, Effect, flow, pipe, Stream } from "effect";
4
+ import type { Scope, Semaphore } from "effect";
5
+
6
+ import type { Statement } from "../adbc-driver.ts";
7
+ import type { SnowflakeSession } from "../models.ts";
8
+ import { failure } from "../snowflake-error.ts";
9
+ import type { Connection } from "./connection.ts";
10
+ import { owned, ownedStream } from "./lifecycle.ts";
11
+ import { call } from "./native.ts";
12
+
13
+ type Reader = Awaited<ReturnType<Statement["executeQuery"]>>;
14
+
15
+ const executeStatement = (statement: Statement) =>
16
+ call("execute", () => statement.executeUpdate());
17
+
18
+ const collectBatches = (source: Reader) =>
19
+ call("query.read", async () => {
20
+ await source.open();
21
+ const { schema } = source;
22
+ const batches: RecordBatch[] = [];
23
+
24
+ for await (const batch of source) {
25
+ batches.push(batch);
26
+ }
27
+
28
+ return new Table(schema, batches);
29
+ });
30
+
31
+ // fromAsyncIterable installs an unsanitized iterator.return finalizer. The scoped
32
+ // reader.cancel owns cleanup; this pull only reads on downstream demand.
33
+ const streamBatches = (source: Reader) =>
34
+ pipe(
35
+ call("stream.read", () => Promise.resolve(source.next())),
36
+ Effect.flatMap((result) =>
37
+ result.done ? Cause.done() : Effect.succeed([result.value] as const)
38
+ ),
39
+ Effect.succeed,
40
+ Stream.fromPull
41
+ );
42
+
43
+ /** Build session operations over a captured connection, permit, and owning scope. */
44
+ export const make = (
45
+ connection: Connection,
46
+ owner: Scope.Scope,
47
+ mutex: Semaphore.Semaphore,
48
+ active: () => boolean
49
+ ): SnowflakeSession => {
50
+ const statement = Effect.fnUntraced(function* acquireStatement(
51
+ sql: string,
52
+ parameters?: Table
53
+ ) {
54
+ if (!active()) {
55
+ return yield* failure("closed")();
56
+ }
57
+
58
+ const resource = yield* Effect.acquireRelease(
59
+ call("statement.create", () => connection.native.createStatement()),
60
+ (value) => connection.cleanup("statement.close", () => value.close())
61
+ );
62
+
63
+ yield* call("statement.sql", () => resource.setSqlQuery(sql));
64
+
65
+ if (parameters !== undefined) {
66
+ yield* call("statement.bind", () => resource.bind(parameters));
67
+ }
68
+
69
+ return resource;
70
+ });
71
+
72
+ const reader = Effect.fnUntraced(function* acquireReader(
73
+ sql: string,
74
+ parameters?: Table
75
+ ) {
76
+ const resource = yield* statement(sql, parameters);
77
+
78
+ return yield* Effect.acquireRelease(
79
+ call("query", () => resource.executeQuery()),
80
+ (value) =>
81
+ connection.cleanup("reader.close", async () => {
82
+ await value.cancel();
83
+ })
84
+ );
85
+ });
86
+
87
+ const run = <A, E, R>(effect: Effect.Effect<A, E, R | Scope.Scope>) =>
88
+ pipe(effect, Effect.scoped, mutex.withPermit, (self) =>
89
+ owned(self, owner, active)
90
+ );
91
+
92
+ const permit = Effect.acquireRelease(mutex.take(1), () => mutex.release(1));
93
+
94
+ return {
95
+ execute: Effect.fn("Snowflake.execute")(
96
+ flow(statement, Effect.flatMap(executeStatement), run)
97
+ ),
98
+ query: Effect.fn("Snowflake.query")(
99
+ flow(reader, Effect.flatMap(collectBatches), run)
100
+ ),
101
+ stream: (sql, parameters) =>
102
+ pipe(
103
+ permit,
104
+ Effect.andThen(reader(sql, parameters)),
105
+ Effect.map(streamBatches),
106
+ Stream.unwrap,
107
+ (self) => ownedStream(self, owner, active),
108
+ Stream.withSpan("Snowflake.stream")
109
+ ),
110
+ };
111
+ };
@@ -0,0 +1,88 @@
1
+ import { Effect, Exit, Semaphore } 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 { asCleanup, owned } from "./lifecycle.ts";
7
+ import { call } from "./native.ts";
8
+ import * as Session from "./session.ts";
9
+
10
+ /** The callback owns its session; closing its scope expires escaped operations. */
11
+ const acquireSession = Effect.fnUntraced(function* acquireTransactionSession(
12
+ connection: Connection
13
+ ) {
14
+ let active = true;
15
+ const owner = yield* Effect.scope;
16
+ yield* Effect.addFinalizer(() =>
17
+ Effect.sync(() => {
18
+ active = false;
19
+ })
20
+ );
21
+ const lock = yield* Semaphore.make(1);
22
+
23
+ return Session.make(
24
+ connection,
25
+ owner,
26
+ lock,
27
+ () => active && connection.isOpen()
28
+ );
29
+ });
30
+
31
+ /** Serialize the whole transaction and finish native finalization before interruption. */
32
+ export const make = (
33
+ connection: Connection,
34
+ mutex: Semaphore.Semaphore
35
+ ): SnowflakeClient["transaction"] => {
36
+ const begin = Effect.suspend(() =>
37
+ connection.isOpen()
38
+ ? connection.setAutoCommit(false)
39
+ : Effect.fail(failure("closed")())
40
+ );
41
+
42
+ const commit = Effect.suspend(() =>
43
+ connection.isHealthy()
44
+ ? call("transaction.commit", () => connection.native.commit())
45
+ : Effect.fail(failure("transaction.invalidated")())
46
+ );
47
+
48
+ const rollback = connection.cleanup("transaction.rollback", () =>
49
+ connection.native.rollback()
50
+ );
51
+
52
+ const restoreAutoCommit = Effect.suspend(() =>
53
+ connection.isHealthy()
54
+ ? connection.setAutoCommit(true).pipe(asCleanup)
55
+ : Effect.void
56
+ );
57
+
58
+ const rollbackOnFailure = (exit: Exit.Exit<unknown, unknown>) =>
59
+ Exit.isFailure(exit) ? rollback : Effect.void;
60
+
61
+ const complete = <A, E, R>(self: Effect.Effect<A, E, R>) =>
62
+ self.pipe(
63
+ Effect.tap(() => commit),
64
+ Effect.onExit((exit) => rollbackOnFailure(exit)),
65
+ Effect.ensuring(restoreAutoCommit)
66
+ );
67
+
68
+ const runBody = <A, E, R>(
69
+ use: (session: SnowflakeSession) => Effect.Effect<A, E, R>
70
+ ) => acquireSession(connection).pipe(Effect.flatMap(use), Effect.scoped);
71
+
72
+ const transact = <A, E, R>(
73
+ use: (session: SnowflakeSession) => Effect.Effect<A, E, R>
74
+ ) =>
75
+ Effect.uninterruptibleMask((restore) => {
76
+ // Only the callback is interruptible; its scope closes before commit.
77
+ const body = restore(runBody(use));
78
+
79
+ return begin.pipe(Effect.andThen(complete(body)));
80
+ });
81
+
82
+ return Effect.fn("Snowflake.transaction")(
83
+ <A, E, R>(use: (session: SnowflakeSession) => Effect.Effect<A, E, R>) =>
84
+ transact(use).pipe(mutex.withPermit, (self) =>
85
+ owned(self, connection.owner, connection.isOpen)
86
+ )
87
+ );
88
+ };
package/src/models.ts ADDED
@@ -0,0 +1,126 @@
1
+ import type { RecordBatch, Table } from "apache-arrow";
2
+ import type { Effect, Redacted, Stream } from "effect";
3
+
4
+ import type { SnowflakeError } from "./snowflake-error.ts";
5
+
6
+ /**
7
+ * Snowflake ADBC connection settings. Named fields override `databaseOptions`.
8
+ * Credentials are revealed only while constructing the native database options.
9
+ * @since 0.1.0
10
+ */
11
+ export interface SnowflakeOptions {
12
+ /** Snowflake account identifier, for example `myorg-myaccount`. */
13
+ readonly account: string;
14
+ /** Login user. Some native authentication methods obtain identity elsewhere. */
15
+ readonly username?: string;
16
+ /** Login password, wrapped in `Redacted`. */
17
+ readonly password?: Redacted.Redacted<string>;
18
+ /** Default database for unqualified SQL names. */
19
+ readonly database?: string;
20
+ /** Default schema within the selected database. */
21
+ readonly schema?: string;
22
+ /** Compute warehouse used to execute queries. */
23
+ readonly warehouse?: string;
24
+ /** Snowflake role to activate for the session. */
25
+ readonly role?: string;
26
+ /** ADBC authentication name; the driver defaults to `auth_snowflake`. These differ from SDK authenticator names. */
27
+ readonly authenticator?:
28
+ | "auth_snowflake"
29
+ | "auth_oauth"
30
+ | "auth_ext_browser"
31
+ | "auth_okta"
32
+ | "auth_jwt"
33
+ | "auth_mfa"
34
+ | "auth_pat"
35
+ | "auth_wif";
36
+ /** Redacted authentication token, passed as the native `auth_token` option. */
37
+ readonly token?: Redacted.Redacted<string>;
38
+ /** Redacted inline PKCS#8 PEM used with `auth_jwt`. */
39
+ readonly privateKey?: Redacted.Redacted<string>;
40
+ /** Redacted passphrase for an encrypted inline PKCS#8 PEM. */
41
+ readonly privateKeyPass?: Redacted.Redacted<string>;
42
+ /** Path to a native-driver-supported private-key file; not inline PEM. */
43
+ readonly privateKeyPath?: string;
44
+ /** Driver name or absolute library/manifest path. Defaults to snowflake. */
45
+ readonly driver?: string;
46
+ /** Additional driver manifest directories, searched before standard locations. */
47
+ readonly manifestSearchPaths?: readonly string[];
48
+ /** Advanced native options. Redact every value, including connection URIs. */
49
+ readonly databaseOptions?: Readonly<
50
+ Record<string, Redacted.Redacted<string>>
51
+ >;
52
+ }
53
+
54
+ /**
55
+ * Serialized operations on a single connection. A transaction session expires
56
+ * when its callback exits. Fully consume a stream before reusing that session.
57
+ * @since 0.1.0
58
+ */
59
+ export interface SnowflakeSession {
60
+ /**
61
+ * Collect a result into an Arrow table, preserving its schema for empty results.
62
+ * @param sql Trusted SQL text with `?` placeholders; values are never interpolated.
63
+ * @param parameters Arrow columns in placeholder order, normally one parameter row.
64
+ * @returns A lazy Effect that owns a fresh statement and reader until collection ends.
65
+ * Native errors become `SnowflakeError`; cleanup failures remain defects.
66
+ */
67
+ readonly query: (
68
+ sql: string,
69
+ parameters?: Table
70
+ ) => Effect.Effect<Table, SnowflakeError>;
71
+ /**
72
+ * Execute DDL or DML using a fresh, scoped statement.
73
+ * @param sql Trusted SQL text with optional `?` placeholders.
74
+ * @param parameters Arrow columns in placeholder order.
75
+ * @returns The affected row count, or `-1` when the native driver cannot report it.
76
+ * Snowflake DDL may implicitly commit; execute it outside transaction callbacks.
77
+ */
78
+ readonly execute: (
79
+ sql: string,
80
+ parameters?: Table
81
+ ) => Effect.Effect<number, SnowflakeError>;
82
+ /**
83
+ * Pull Arrow record batches lazily without collecting the full result.
84
+ * @param sql Trusted SQL text with optional `?` placeholders.
85
+ * @param parameters Arrow columns in placeholder order.
86
+ * @returns A stream that holds the connection until consumption and cleanup finish.
87
+ * Early termination releases the reader before its statement. In-flight native
88
+ * reads settle before interruption completes; ADBC JS provides no cancellation.
89
+ */
90
+ readonly stream: (
91
+ sql: string,
92
+ parameters?: Table
93
+ ) => Stream.Stream<RecordBatch, SnowflakeError>;
94
+ }
95
+
96
+ /**
97
+ * A scoped connection with exclusive transaction callbacks.
98
+ * @since 0.1.0
99
+ */
100
+ export interface SnowflakeClient extends SnowflakeSession {
101
+ /**
102
+ * Run an Effect inside this client's transaction. Composes with `pipe` and
103
+ * preserves its success, errors, and environment. Captured repositories and
104
+ * client methods automatically use the active session on this fiber.
105
+ */
106
+ readonly withTransaction: <A, E, R>(
107
+ self: Effect.Effect<A, E, R>
108
+ ) => Effect.Effect<A, E | SnowflakeError, R>;
109
+
110
+ /**
111
+ * Disable autocommit, run `use`, and commit its success or roll back its failure.
112
+ * @param use Callback using the supplied session; await all work before returning.
113
+ * @returns The callback value, preserving its error and service requirements.
114
+ * Defects and callback interruption also roll back. Once commit starts, its
115
+ * outcome and autocommit restoration finish before interruption is accepted;
116
+ * an interrupted caller may therefore observe an already committed transaction.
117
+ * Failed commits attempt rollback. Recovered cleanup defects still prevent
118
+ * commit and fail with operation `transaction.invalidated`.
119
+ * Failed rollback or autocommit restoration invalidates the client, and cleanup
120
+ * defects accompany the original failure. Captured client methods resolve the
121
+ * active transaction session. Nested transactions fail with `transaction.nested`.
122
+ */
123
+ readonly transaction: <A, E, R>(
124
+ use: (session: SnowflakeSession) => Effect.Effect<A, E, R>
125
+ ) => Effect.Effect<A, E | SnowflakeError, R>;
126
+ }
@@ -0,0 +1,17 @@
1
+ import { Schema } from "effect";
2
+
3
+ /**
4
+ * Sanitized native failure with an operation label and a generic message.
5
+ * Raw native messages, SQL, bind values, credentials, and original causes are
6
+ * intentionally excluded. Operation failures use the typed error channel;
7
+ * cleanup failures are promoted to defects while preserving the original Cause.
8
+ * @since 0.1.0
9
+ */
10
+ // oxlint-disable-next-line unicorn/throw-new-error -- Schema.TaggedError is a class factory.
11
+ export class SnowflakeError extends Schema.TaggedError<
12
+ SnowflakeError,
13
+ { readonly brand: unique symbol }
14
+ >()("SnowflakeError", { message: Schema.String, operation: Schema.String }) {}
15
+
16
+ export const failure = (operation: string) => () =>
17
+ new SnowflakeError({ message: `Snowflake ${operation} failed`, operation });