@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.
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +174 -2
- package/dist/adbc-driver.d.ts +49 -0
- package/dist/adbc-driver.d.ts.map +1 -0
- package/dist/adbc-driver.js +29 -0
- package/dist/adbc-driver.js.map +1 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +89 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/client.d.ts +6 -0
- package/dist/internal/client.d.ts.map +1 -0
- package/dist/internal/client.js +29 -0
- package/dist/internal/client.js.map +1 -0
- package/dist/internal/configuration.d.ts +4 -0
- package/dist/internal/configuration.d.ts.map +1 -0
- package/dist/internal/configuration.js +51 -0
- package/dist/internal/configuration.js.map +1 -0
- package/dist/internal/connection.d.ts +25 -0
- package/dist/internal/connection.d.ts.map +1 -0
- package/dist/internal/connection.js +39 -0
- package/dist/internal/connection.js.map +1 -0
- package/dist/internal/lifecycle.d.ts +9 -0
- package/dist/internal/lifecycle.d.ts.map +1 -0
- package/dist/internal/lifecycle.js +81 -0
- package/dist/internal/lifecycle.js.map +1 -0
- package/dist/internal/native.d.ts +4 -0
- package/dist/internal/native.d.ts.map +1 -0
- package/dist/internal/native.js +5 -0
- package/dist/internal/native.js.map +1 -0
- package/dist/internal/session.d.ts +6 -0
- package/dist/internal/session.d.ts.map +1 -0
- package/dist/internal/session.js +46 -0
- package/dist/internal/session.js.map +1 -0
- package/dist/internal/transaction.d.ts +6 -0
- package/dist/internal/transaction.d.ts.map +1 -0
- package/dist/internal/transaction.js +38 -0
- package/dist/internal/transaction.js.map +1 -0
- package/dist/models.d.ts +99 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +2 -0
- package/dist/models.js.map +1 -0
- package/dist/snowflake-error.d.ts +19 -0
- package/dist/snowflake-error.d.ts.map +1 -0
- package/dist/snowflake-error.js +13 -0
- package/dist/snowflake-error.js.map +1 -0
- package/examples/.env.example +16 -0
- package/examples/README.md +73 -0
- package/examples/check.ts +12 -0
- package/examples/composition.ts +24 -0
- package/examples/options.ts +80 -0
- package/examples/query.ts +4 -0
- package/examples/run.ts +30 -0
- package/examples/scenarios.ts +114 -0
- package/examples/stream.ts +4 -0
- package/examples/transaction.ts +4 -0
- package/package.json +59 -4
- package/src/adbc-driver.ts +83 -0
- package/src/index.ts +151 -0
- package/src/internal/client.ts +69 -0
- package/src/internal/configuration.ts +63 -0
- package/src/internal/connection.ts +83 -0
- package/src/internal/lifecycle.ts +186 -0
- package/src/internal/native.ts +9 -0
- package/src/internal/session.ts +111 -0
- package/src/internal/transaction.ts +88 -0
- package/src/models.ts +126 -0
- 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
|
+
);
|
package/examples/run.ts
ADDED
|
@@ -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
|
+
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rc312/effect-snowflake",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
});
|