@uniflowed/sql 0.14.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.
@@ -0,0 +1,191 @@
1
+ // @flow
2
+ //
3
+ // Checks every engine's codecs share. Each one either returns the value it was
4
+ // asked for or throws a `SqlError` that names what it expected — never a
5
+ // rounded number, a `NaN` or an `Invalid Date` handed on as if it were data.
6
+
7
+ import type { JsonValue } from "../index.js";
8
+ import { decodeError, encodeError } from "../index.js";
9
+
10
+ const INTEGER = /^[-+]?\d+$/;
11
+
12
+ /** A string of decimal digits as a `number`, which must be exactly representable. */
13
+ export function integerFromText(expected: string, value: mixed): number {
14
+ if (typeof value === "number") {
15
+ return safeInteger(expected, value);
16
+ }
17
+ if (typeof value === "bigint") {
18
+ return safeInteger(expected, value);
19
+ }
20
+ if (typeof value !== "string" || !INTEGER.test(value)) {
21
+ return decodeError(expected, value);
22
+ }
23
+ const parsed = Number(value);
24
+ if (!Number.isSafeInteger(parsed)) {
25
+ return decodeError(`${expected} within ±2^53 (use the bigint representation)`, value);
26
+ }
27
+ return parsed;
28
+ }
29
+
30
+ /** A string of decimal digits, a `bigint` or a safe `number` as a `bigint`. */
31
+ export function bigintFrom(expected: string, value: mixed): bigint {
32
+ if (typeof value === "bigint") {
33
+ return value;
34
+ }
35
+ if (typeof value === "number") {
36
+ // A number past 2^53 may already have been rounded by the driver, so it
37
+ // cannot be trusted to be the integer the database holds.
38
+ if (!Number.isSafeInteger(value)) {
39
+ return decodeError(`${expected} (the driver returned a rounded number)`, value);
40
+ }
41
+ return BigInt(value);
42
+ }
43
+ if (typeof value !== "string" || !INTEGER.test(value)) {
44
+ return decodeError(expected, value);
45
+ }
46
+ return BigInt(value);
47
+ }
48
+
49
+ /** A `number` or `bigint` that is an integer a `number` holds exactly. */
50
+ export function safeInteger(expected: string, value: number | bigint): number {
51
+ if (typeof value === "bigint") {
52
+ const asNumber = Number(value);
53
+ if (!Number.isSafeInteger(asNumber)) {
54
+ return decodeError(`${expected} within ±2^53 (use the bigint representation)`, value);
55
+ }
56
+ return asNumber;
57
+ }
58
+ if (!Number.isSafeInteger(value)) {
59
+ return decodeError(`${expected} within ±2^53 (use the bigint representation)`, value);
60
+ }
61
+ return value;
62
+ }
63
+
64
+ /** Encode a `number` that must be an integer, for an integer column. */
65
+ export function integerParam(expected: string, value: mixed): number {
66
+ if (typeof value !== "number" || !Number.isSafeInteger(value)) {
67
+ return encodeError(expected, value);
68
+ }
69
+ return value;
70
+ }
71
+
72
+ /** Encode a `bigint`. */
73
+ export function bigintParam(expected: string, value: mixed): bigint {
74
+ if (typeof value !== "bigint") {
75
+ return encodeError(expected, value);
76
+ }
77
+ return value;
78
+ }
79
+
80
+ /** A string, unchanged. */
81
+ export function stringFrom(expected: string, value: mixed): string {
82
+ if (typeof value !== "string") {
83
+ return decodeError(expected, value);
84
+ }
85
+ return value;
86
+ }
87
+
88
+ /** Encode a string. */
89
+ export function stringParam(expected: string, value: mixed): string {
90
+ if (typeof value !== "string") {
91
+ return encodeError(expected, value);
92
+ }
93
+ return value;
94
+ }
95
+
96
+ /** Encode a finite or non-finite `number`. */
97
+ export function numberParam(expected: string, value: mixed): number {
98
+ if (typeof value !== "number") {
99
+ return encodeError(expected, value);
100
+ }
101
+ return value;
102
+ }
103
+
104
+ /** Parse a JSON document the database returned as text. */
105
+ export function jsonFromText(value: mixed): JsonValue {
106
+ if (typeof value !== "string") {
107
+ return decodeError("JSON text", value);
108
+ }
109
+ try {
110
+ const parsed: JsonValue = JSON.parse(value);
111
+ return parsed;
112
+ } catch {
113
+ return decodeError("JSON text", value);
114
+ }
115
+ }
116
+
117
+ /** Serialise a JSON parameter. `undefined`, functions and symbols have no JSON. */
118
+ export function jsonParam(value: mixed): string {
119
+ let text: string | void;
120
+ try {
121
+ text = JSON.stringify(value);
122
+ } catch {
123
+ // A cycle, or a `bigint`, which JSON has no spelling for.
124
+ return encodeError("a JSON value", value);
125
+ }
126
+ if (text === undefined) {
127
+ return encodeError("a JSON value", value);
128
+ }
129
+ return text;
130
+ }
131
+
132
+ /** Bytes, from a `Uint8Array` (a Node `Buffer` is one), an `ArrayBuffer` or an array of octets. */
133
+ export function bytesFrom(expected: string, value: mixed): Uint8Array {
134
+ if (value instanceof Uint8Array) {
135
+ return value;
136
+ }
137
+ if (value instanceof ArrayBuffer) {
138
+ return new Uint8Array(value);
139
+ }
140
+ if (Array.isArray(value)) {
141
+ const out = new Uint8Array(value.length);
142
+ for (let i = 0; i < value.length; i += 1) {
143
+ const octet = value[i];
144
+ if (typeof octet !== "number" || !Number.isInteger(octet) || octet < 0 || octet > 255) {
145
+ return decodeError(expected, value);
146
+ }
147
+ out[i] = octet;
148
+ }
149
+ return out;
150
+ }
151
+ return decodeError(expected, value);
152
+ }
153
+
154
+ /** Encode bytes. */
155
+ export function bytesParam(expected: string, value: mixed): Uint8Array {
156
+ if (!(value instanceof Uint8Array)) {
157
+ return encodeError(expected, value);
158
+ }
159
+ return value;
160
+ }
161
+
162
+ /** One of an enum's labels. */
163
+ export function label<T extends string>(
164
+ values: $ReadOnlyArray<T>,
165
+ expected: string,
166
+ value: mixed,
167
+ ): T {
168
+ const index = typeof value === "string" ? values.findIndex((item) => item === value) : -1;
169
+ if (index < 0) {
170
+ return decodeError(expected, value);
171
+ }
172
+ return values[index];
173
+ }
174
+
175
+ /** Encode one of an enum's labels, refusing a string that is not one. */
176
+ export function labelParam<T extends string>(
177
+ values: $ReadOnlyArray<T>,
178
+ expected: string,
179
+ value: mixed,
180
+ ): T {
181
+ const index = typeof value === "string" ? values.findIndex((item) => item === value) : -1;
182
+ if (index < 0) {
183
+ return encodeError(expected, value);
184
+ }
185
+ return values[index];
186
+ }
187
+
188
+ /** How an enum is described in a message. */
189
+ export function enumName(values: $ReadOnlyArray<string>): string {
190
+ return `one of ${values.map((value) => JSON.stringify(value)).join(", ")}`;
191
+ }
@@ -0,0 +1,55 @@
1
+ // @flow
2
+ //
3
+ // A bounded cache of prepared statements, for the SQLite adapters.
4
+ //
5
+ // Generated code runs the same few dozen statements over and over, and
6
+ // preparing one costs a parse and a plan. The texts are constants in the
7
+ // generated modules, so the cache's key space is the application's queries —
8
+ // except for `sqlc.slice` and `:copyfrom`, whose text grows with the input.
9
+ // The bound keeps those from growing it without limit; the least recently
10
+ // used statement goes first.
11
+
12
+ export const DEFAULT_STATEMENT_CACHE = 256;
13
+
14
+ export function statementCache<S>(
15
+ prepare: (text: string) => S,
16
+ limit: number,
17
+ ): (text: string) => S {
18
+ const cache: Map<string, S> = new Map();
19
+ return (text) => {
20
+ const hit = cache.get(text);
21
+ if (hit !== undefined) {
22
+ // Re-insert, so iteration order is least recently used first.
23
+ cache.delete(text);
24
+ cache.set(text, hit);
25
+ return hit;
26
+ }
27
+ const statement = prepare(text);
28
+ if (limit > 0) {
29
+ if (cache.size >= limit) {
30
+ const oldest = cache.keys().next();
31
+ if (!oldest.done) {
32
+ cache.delete(oldest.value);
33
+ }
34
+ }
35
+ cache.set(text, statement);
36
+ }
37
+ return statement;
38
+ };
39
+ }
40
+
41
+ /** A driver's row count or row id, which may be a `number` or a `bigint`, as a `number`. */
42
+ export function count(value: number | bigint): number {
43
+ return typeof value === "bigint" ? Number(value) : value;
44
+ }
45
+
46
+ /** A driver's last row id as a `bigint`, or `null` when it reported none. */
47
+ export function rowId(value: mixed): bigint | null {
48
+ if (typeof value === "bigint") {
49
+ return value;
50
+ }
51
+ if (typeof value === "number" && Number.isSafeInteger(value)) {
52
+ return BigInt(value);
53
+ }
54
+ return null;
55
+ }
package/mysql.js ADDED
@@ -0,0 +1,231 @@
1
+ // @flow
2
+ //
3
+ // `@uniflowed/sql/mysql`: MySQL's text representation, both ways.
4
+ //
5
+ // The `mysql2` adapter reads every column as the string MySQL sent (binary
6
+ // types as bytes) and sends parameters as strings, which MySQL converts to the
7
+ // column's type — the conversion it applies to any literal, and the one that
8
+ // keeps a `BIGINT UNSIGNED` or a `DECIMAL(65,30)` exact on the way in and out.
9
+ //
10
+ // `DATETIME` and `TIMESTAMP` stay strings. A `TIMESTAMP` is stored in UTC and
11
+ // printed in the session's time zone, so the wall-clock text is the only
12
+ // representation that does not depend on a setting of a connection the
13
+ // generated code never sees.
14
+
15
+ import type { JsonValue } from "./index.js";
16
+ import { decodeError, encodeError } from "./index.js";
17
+ import {
18
+ bigintFrom,
19
+ bytesFrom,
20
+ bytesParam,
21
+ enumName,
22
+ integerFromText,
23
+ jsonFromText,
24
+ jsonParam,
25
+ label,
26
+ labelParam,
27
+ stringFrom,
28
+ stringParam,
29
+ } from "./internal/shared.js";
30
+
31
+ /** What a MySQL parameter is sent as. */
32
+ export type MysqlParam = null | string | Uint8Array;
33
+
34
+ /** How one MySQL type becomes a Flow value and back. */
35
+ export type Codec<T> = {|
36
+ readonly sqlType: string,
37
+ readonly decode: (value: mixed) => T,
38
+ readonly encode: (value: T) => MysqlParam,
39
+ |};
40
+
41
+ /** `TINYINT` through `INT`, signed or unsigned, and `YEAR`: all fit a `number`. */
42
+ export function integer(sqlType: string): Codec<number> {
43
+ return {
44
+ sqlType,
45
+ decode: (value) => integerFromText(sqlType, value),
46
+ encode: (value) => {
47
+ if (!Number.isSafeInteger(value)) {
48
+ return encodeError(`${sqlType} (an integer)`, value);
49
+ }
50
+ return String(value);
51
+ },
52
+ };
53
+ }
54
+
55
+ /** `BIGINT`, signed or unsigned, exactly. */
56
+ export const bigint: Codec<bigint> = {
57
+ sqlType: "bigint",
58
+ decode: (value) => bigintFrom("bigint", value),
59
+ encode: (value) => {
60
+ if (typeof value !== "bigint") {
61
+ return encodeError("bigint (a bigint)", value);
62
+ }
63
+ return String(value);
64
+ },
65
+ };
66
+
67
+ /** `BIGINT` as a `number`, for `int8: "number"`; throws past 2^53. */
68
+ export const bigintAsNumber: Codec<number> = integer("bigint");
69
+
70
+ /** `BIGINT` as its digits, for `int8: "string"`. */
71
+ export const bigintAsString: Codec<string> = {
72
+ sqlType: "bigint",
73
+ decode: (value) => {
74
+ if (typeof value === "string" && /^[-+]?\d+$/.test(value)) {
75
+ return value;
76
+ }
77
+ return decodeError("bigint", value);
78
+ },
79
+ encode: (value) => stringParam("bigint (a string of digits)", value),
80
+ };
81
+
82
+ /** `TINYINT(1)` and `BOOL`, which MySQL spells `1` and `0`. */
83
+ export const boolean: Codec<boolean> = {
84
+ sqlType: "tinyint(1)",
85
+ decode: (value) => {
86
+ if (value === "0" || value === 0) {
87
+ return false;
88
+ }
89
+ if (typeof value === "string" && /^-?\d+$/.test(value)) {
90
+ // MySQL's own truth test: any non-zero `TINYINT(1)` is true.
91
+ return true;
92
+ }
93
+ if (typeof value === "number" && Number.isInteger(value)) {
94
+ return true;
95
+ }
96
+ return decodeError("tinyint(1)", value);
97
+ },
98
+ encode: (value) => {
99
+ if (typeof value !== "boolean") {
100
+ return encodeError("tinyint(1) (a boolean)", value);
101
+ }
102
+ return value ? "1" : "0";
103
+ },
104
+ };
105
+
106
+ const DECIMAL = /^[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?$/;
107
+
108
+ /** `DECIMAL`/`NUMERIC` as its exact decimal string. */
109
+ export const decimal: Codec<string> = {
110
+ sqlType: "decimal",
111
+ decode: (value) => {
112
+ if (typeof value === "string" && DECIMAL.test(value)) {
113
+ return value;
114
+ }
115
+ return decodeError("decimal", value);
116
+ },
117
+ encode: (value) => {
118
+ if (typeof value !== "string" || !DECIMAL.test(value)) {
119
+ return encodeError("decimal (a decimal string)", value);
120
+ }
121
+ return value;
122
+ },
123
+ };
124
+
125
+ /** `FLOAT`, `DOUBLE`, and `DECIMAL` for `numeric: "number"`. */
126
+ export function float(sqlType: string): Codec<number> {
127
+ return {
128
+ sqlType,
129
+ decode: (value) => {
130
+ if (typeof value === "number") {
131
+ return value;
132
+ }
133
+ if (typeof value === "string" && DECIMAL.test(value)) {
134
+ return Number(value);
135
+ }
136
+ return decodeError(sqlType, value);
137
+ },
138
+ encode: (value) => {
139
+ if (typeof value !== "number" || !Number.isFinite(value)) {
140
+ // MySQL has no NaN or infinity.
141
+ return encodeError(`${sqlType} (a finite number)`, value);
142
+ }
143
+ return String(value);
144
+ },
145
+ };
146
+ }
147
+
148
+ /** Character types, `DATE`, `DATETIME`, `TIMESTAMP`, `TIME`, `SET`. */
149
+ export function text(sqlType: string): Codec<string> {
150
+ return {
151
+ sqlType,
152
+ decode: (value) => stringFrom(sqlType, value),
153
+ encode: (value) => stringParam(`${sqlType} (a string)`, value),
154
+ };
155
+ }
156
+
157
+ /** The character types. */
158
+ export const string: Codec<string> = text("varchar");
159
+
160
+ /** `BLOB`, `BINARY`, `VARBINARY`, `BIT`. */
161
+ export function bytes(sqlType: string): Codec<Uint8Array> {
162
+ return {
163
+ sqlType,
164
+ decode: (value) => bytesFrom(sqlType, value),
165
+ encode: (value) => bytesParam(`${sqlType} (a Uint8Array)`, value),
166
+ };
167
+ }
168
+
169
+ /** `JSON`. */
170
+ export const json: Codec<JsonValue> = {
171
+ sqlType: "json",
172
+ decode: (value) => jsonFromText(value),
173
+ encode: (value) => jsonParam(value),
174
+ };
175
+
176
+ /** A result sqlc could not type. */
177
+ export const unknown: Codec<mixed> = {
178
+ sqlType: "unknown",
179
+ decode: (value) => value,
180
+ encode: (value) => {
181
+ if (value === null || typeof value === "string" || value instanceof Uint8Array) {
182
+ return value;
183
+ }
184
+ if (typeof value === "number" || typeof value === "bigint") {
185
+ return String(value);
186
+ }
187
+ if (typeof value === "boolean") {
188
+ return value ? "1" : "0";
189
+ }
190
+ return jsonParam(value);
191
+ },
192
+ };
193
+
194
+ /** An `ENUM(…)` column, from its values in declaration order. */
195
+ export function enumeration<T extends string>(
196
+ sqlType: string,
197
+ values: $ReadOnlyArray<T>,
198
+ ): Codec<T> {
199
+ const expected = `${sqlType} (${enumName(values)})`;
200
+ return {
201
+ sqlType,
202
+ decode: (value) => label(values, expected, value),
203
+ encode: (value) => labelParam(values, expected, value),
204
+ };
205
+ }
206
+
207
+ /**
208
+ * An override's codec: `decode` narrows what `codec` read, `encode` widens a
209
+ * value back to what `codec` writes.
210
+ */
211
+ export function map<T, U>(
212
+ codec: Codec<T>,
213
+ decode: (value: T) => U,
214
+ encode: (value: U) => T,
215
+ ): Codec<U> {
216
+ return {
217
+ sqlType: codec.sqlType,
218
+ decode: (value) => decode(codec.decode(value)),
219
+ encode: (value) => codec.encode(encode(value)),
220
+ };
221
+ }
222
+
223
+ /** Decode a nullable column. */
224
+ export function nullable<T>(codec: Codec<T>, value: mixed): T | null {
225
+ return value === null ? null : codec.decode(value);
226
+ }
227
+
228
+ /** Encode a nullable parameter. */
229
+ export function param<T>(codec: Codec<T>, value: T | null): MysqlParam {
230
+ return value === null ? null : codec.encode(value);
231
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@uniflowed/sql",
3
+ "version": "0.14.0",
4
+ "description": "The runtime for sqlc's Flow target: exact codecs and driver adapters, part of the Unified Toolchain for Flow.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "sideEffects": false,
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/ubugeeei-prod/uf.git",
11
+ "directory": "npm/sql"
12
+ },
13
+ "exports": {
14
+ ".": "./index.js",
15
+ "./postgresql": "./postgresql.js",
16
+ "./mysql": "./mysql.js",
17
+ "./sqlite": "./sqlite.js",
18
+ "./node-sqlite": "./adapters/node-sqlite.js",
19
+ "./bun-sqlite": "./adapters/bun-sqlite.js",
20
+ "./better-sqlite3": "./adapters/better-sqlite3.js",
21
+ "./d1": "./adapters/d1.js",
22
+ "./pglite": "./adapters/pglite.js",
23
+ "./pg": "./adapters/pg.js",
24
+ "./postgres": "./adapters/postgres.js",
25
+ "./mysql2": "./adapters/mysql2.js"
26
+ },
27
+ "files": [
28
+ "*.js",
29
+ "adapters",
30
+ "internal",
31
+ "!*.test.js"
32
+ ]
33
+ }