@yydb/sqlite 0.0.0 → 0.0.2
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/bin/sqlite3.mjs +119 -0
- package/package.json +56 -6
- package/readme.md +31 -0
- package/src/disguise.ts +3 -0
- package/src/index.ts +12 -0
- package/src/node.ts +195 -0
- package/README.md +0 -3
package/bin/sqlite3.mjs
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
import { SqliteDatabase, SqliteSnapshot } from "../src/node.ts";
|
|
5
|
+
|
|
6
|
+
const FORMAT_VERSION = "3.45.0";
|
|
7
|
+
|
|
8
|
+
function runDotCommand(database, command) {
|
|
9
|
+
if (command === ".tables") {
|
|
10
|
+
const tables = database
|
|
11
|
+
.schema()
|
|
12
|
+
.filter((object) => (object.kind === "table" || object.kind === "view") && !object.name.startsWith("sqlite_"))
|
|
13
|
+
.map((object) => object.name)
|
|
14
|
+
.sort();
|
|
15
|
+
for (const name of tables) console.log(name);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (command === ".schema") {
|
|
19
|
+
const statements = database
|
|
20
|
+
.schema()
|
|
21
|
+
.filter((object) => object.sql && !object.name.startsWith("sqlite_"))
|
|
22
|
+
.map((object) => object.sql?.trimEnd() ?? "");
|
|
23
|
+
for (const source of statements) {
|
|
24
|
+
console.log(source.endsWith(";") ? source : `${source};`);
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (command === ".ping") {
|
|
29
|
+
const status = database.ping();
|
|
30
|
+
console.log(`ok ${status}`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (command === ".encoding") {
|
|
34
|
+
console.log(database.encoding());
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (command === ".version") {
|
|
38
|
+
console.log(database.sqliteVersion());
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const dump = command.match(/^\.dump\s+(\S+)\s*$/);
|
|
42
|
+
if (dump) {
|
|
43
|
+
const table = dump[1];
|
|
44
|
+
const rows = database.table(table);
|
|
45
|
+
if (rows.length === 0) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const width = new Array(rows[0].columns.length).fill(0);
|
|
49
|
+
for (const row of rows) {
|
|
50
|
+
for (let i = 0; i < row.columns.length; i++) {
|
|
51
|
+
const text = formatValue(row.columns[i]);
|
|
52
|
+
width[i] = Math.max(width[i], text.length);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const line = (cells) => cells.map((c, i) => c.padEnd(width[i])).join(" | ");
|
|
56
|
+
for (const row of rows) {
|
|
57
|
+
console.log(
|
|
58
|
+
line([
|
|
59
|
+
String(row.rowid),
|
|
60
|
+
...row.columns.map(formatValue),
|
|
61
|
+
]),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
throw new Error(`unsupported dot command '${command}'`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatValue(value) {
|
|
70
|
+
if (value.kind === "null") return "NULL";
|
|
71
|
+
if (value.kind === "integer") return String(value.valueInteger ?? "");
|
|
72
|
+
if (value.kind === "real") {
|
|
73
|
+
const n = value.valueReal;
|
|
74
|
+
return n === undefined ? "" : (Number.isInteger(n) ? `${n}.0` : String(n));
|
|
75
|
+
}
|
|
76
|
+
if (value.kind === "text") return value.valueText ?? "";
|
|
77
|
+
if (value.kind === "blob") {
|
|
78
|
+
const buf = value.valueBlob;
|
|
79
|
+
if (!buf) return "";
|
|
80
|
+
let hex = "";
|
|
81
|
+
for (let i = 0; i < Math.min(buf.length, 64); i++) {
|
|
82
|
+
hex += buf[i].toString(16).padStart(2, "0");
|
|
83
|
+
}
|
|
84
|
+
return `X'${hex}${buf.length > 64 ? "…" : ""}'`;
|
|
85
|
+
}
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function main(args) {
|
|
90
|
+
if (args.length === 1 && ["--version", "-version"].includes(args[0])) {
|
|
91
|
+
console.log(`sqlite3 ${FORMAT_VERSION} (YYDS pure-Rust format-3 reader)`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (args.length === 1 && ["--help", "-help"].includes(args[0])) {
|
|
95
|
+
console.log(
|
|
96
|
+
"Usage: sqlite3 DATABASE COMMAND\n" +
|
|
97
|
+
"Supported dot commands: .tables, .schema, .ping, .encoding, .version, .dump TABLE\n" +
|
|
98
|
+
"This CLI inspects on-disk main-file bytes only; SQL execution is not available.",
|
|
99
|
+
);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (args.length !== 2 || args[0].startsWith("-") || args[0] === "") {
|
|
103
|
+
throw new Error("expected DATABASE and one supported dot command");
|
|
104
|
+
}
|
|
105
|
+
const [path, input] = args;
|
|
106
|
+
const command = input.trim();
|
|
107
|
+
if (!command.startsWith(".")) {
|
|
108
|
+
throw new Error("SQL execution is not available; use a dot command");
|
|
109
|
+
}
|
|
110
|
+
const database = SqliteDatabase.open(path);
|
|
111
|
+
runDotCommand(database, command);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
main(process.argv.slice(2));
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error(`@yydb/sqlite: ${error.message}`);
|
|
118
|
+
process.exitCode = 1;
|
|
119
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yydb/sqlite",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
5
|
-
"license": "
|
|
6
|
-
"
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Bounded read-only SQLite format-3 snapshot reader, independent of YYDS storage.",
|
|
5
|
+
"license": "MPL-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"import": "./src/index.ts",
|
|
12
|
+
"default": "./src/index.ts"
|
|
13
|
+
},
|
|
14
|
+
"./node": {
|
|
15
|
+
"types": "./src/node.ts",
|
|
16
|
+
"import": "./src/node.ts",
|
|
17
|
+
"default": "./src/node.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"sqlite3": "./bin/sqlite3.mjs"
|
|
22
|
+
},
|
|
7
23
|
"files": [
|
|
8
|
-
"
|
|
9
|
-
|
|
24
|
+
"bin",
|
|
25
|
+
"src",
|
|
26
|
+
"readme.md"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
30
|
+
"test": "node --experimental-strip-types --test \"./tests/**/*.test.ts\""
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/yy-database/yyds.git",
|
|
35
|
+
"directory": "projects/packages/sqlite"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/yy-database/yyds/tree/dev/projects/packages/sqlite#readme",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/yy-database/yyds/issues"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"yyds",
|
|
43
|
+
"sqlite",
|
|
44
|
+
"disguise",
|
|
45
|
+
"binary-compatible"
|
|
46
|
+
],
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@yydb/yyds": "0.0.2"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^22.15.0",
|
|
55
|
+
"typescript": "^5.9.2"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=18"
|
|
59
|
+
}
|
|
10
60
|
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @yydb/sqlite
|
|
2
|
+
|
|
3
|
+
Read-only inspection of SQLite format-3 main files. Copies bytes into memory, lists schema objects, and reads raw table records. Does not execute SQL, open a live database, or recover WAL sidecars.
|
|
4
|
+
|
|
5
|
+
SQLite files are not YYDS storage. They are not routed through `.yyds` catalogs or `.yykv` shards.
|
|
6
|
+
|
|
7
|
+
## Exports
|
|
8
|
+
|
|
9
|
+
| Subpath | Use |
|
|
10
|
+
|---------|-----|
|
|
11
|
+
| `@yydb/sqlite` | Types only |
|
|
12
|
+
| `@yydb/sqlite/node` | `SqliteSnapshot` (requires the `@yydb/yyds` native addon) |
|
|
13
|
+
|
|
14
|
+
The `sqlite3` command supports `.tables` and `.schema` on a file path. It does not run SQL statements.
|
|
15
|
+
|
|
16
|
+
## Example
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
import { SqliteSnapshot } from "@yydb/sqlite/node";
|
|
21
|
+
|
|
22
|
+
const snapshot = new SqliteSnapshot(readFileSync("app.sqlite"));
|
|
23
|
+
const tables = snapshot.schema().filter((o) => o.kind === "table");
|
|
24
|
+
const rows = snapshot.table("samples", { maxRows: 100 });
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Each row is `{ rowid: bigint, payload: Buffer }` — storage bytes, not SQL-projected values. Schema `sql` fields are opaque CREATE text. The default snapshot size cap is 256 MiB; each scan accepts `maxPages`, `maxRows`, `maxPayloadBytes`, and `maxTotalPayloadBytes`.
|
|
28
|
+
|
|
29
|
+
## Related packages
|
|
30
|
+
|
|
31
|
+
- [`@yydb/yyds`](https://github.com/yy-database/yyds/tree/dev/projects/packages/yyds) — hosts the native `SqliteSnapshot` binding
|
package/src/disguise.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
SqliteDatabaseBinding,
|
|
3
|
+
SqliteDecodedRow,
|
|
4
|
+
SqliteReadLimits,
|
|
5
|
+
SqliteRecordValue,
|
|
6
|
+
SqliteSchemaObject,
|
|
7
|
+
SqliteSnapshotBinding,
|
|
8
|
+
SqliteTableRow,
|
|
9
|
+
SqliteTextEncoding,
|
|
10
|
+
} from "./node.ts";
|
|
11
|
+
|
|
12
|
+
export { SqliteSnapshot, SqliteDatabase, decodeRecord } from "./node.ts";
|
package/src/node.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { loadYydsSqliteNative } from "@yydb/yyds/node";
|
|
2
|
+
import type {
|
|
3
|
+
SqliteDatabaseBinding,
|
|
4
|
+
SqliteDecodedRow,
|
|
5
|
+
SqliteReadLimits,
|
|
6
|
+
SqliteRecordValue,
|
|
7
|
+
SqliteSchemaObject,
|
|
8
|
+
SqliteSnapshotBinding,
|
|
9
|
+
SqliteTableRow,
|
|
10
|
+
} from "@yydb/yyds/node";
|
|
11
|
+
|
|
12
|
+
export type {
|
|
13
|
+
SqliteDatabaseBinding,
|
|
14
|
+
SqliteDecodedRow,
|
|
15
|
+
SqliteReadLimits,
|
|
16
|
+
SqliteRecordValue,
|
|
17
|
+
SqliteSchemaObject,
|
|
18
|
+
SqliteSnapshotBinding,
|
|
19
|
+
SqliteTableRow,
|
|
20
|
+
} from "@yydb/yyds/node";
|
|
21
|
+
|
|
22
|
+
/** Text encodings recognised by the SQLite header. */
|
|
23
|
+
export type SqliteTextEncoding = "utf8" | "utf16le" | "utf16be";
|
|
24
|
+
|
|
25
|
+
/** Read-only, copied main-file bytes. WAL/journal recovery is not performed. */
|
|
26
|
+
export class SqliteSnapshot {
|
|
27
|
+
readonly #native: SqliteSnapshotBinding;
|
|
28
|
+
|
|
29
|
+
constructor(bytes: Uint8Array, maxSnapshotBytes = 256 * 1024 * 1024) {
|
|
30
|
+
unsignedLimit(maxSnapshotBytes, "maxSnapshotBytes");
|
|
31
|
+
if (!(bytes instanceof Uint8Array)) {
|
|
32
|
+
throw new TypeError("SQLite snapshot must be a Uint8Array or Buffer");
|
|
33
|
+
}
|
|
34
|
+
if (bytes.byteLength > maxSnapshotBytes) {
|
|
35
|
+
throw new RangeError("sqlite snapshot byte limit exceeded");
|
|
36
|
+
}
|
|
37
|
+
const native = loadYydsSqliteNative();
|
|
38
|
+
this.#native = new native.SqliteSnapshot(Buffer.from(bytes), maxSnapshotBytes);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
schema(limits?: SqliteReadLimits): SqliteSchemaObject[] {
|
|
42
|
+
return this.#native.schema(validateLimits(limits));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
table(name: string, limits?: SqliteReadLimits): SqliteTableRow[] {
|
|
46
|
+
if (typeof name !== "string") {
|
|
47
|
+
throw new TypeError("SQLite table name must be a string");
|
|
48
|
+
}
|
|
49
|
+
return this.#native.table(name, validateLimits(limits));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Reads rows with decoded storage-class values. */
|
|
53
|
+
tableDecoded(name: string, limits?: SqliteReadLimits): SqliteDecodedRow[] {
|
|
54
|
+
if (typeof name !== "string") {
|
|
55
|
+
throw new TypeError("SQLite table name must be a string");
|
|
56
|
+
}
|
|
57
|
+
return this.#native.tableDecoded(name, validateLimits(limits));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** SQLite library version stamped in the snapshot header. */
|
|
61
|
+
sqliteVersion(): string {
|
|
62
|
+
return this.#native.sqliteVersion();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Returns the header-claimed text encoding. */
|
|
66
|
+
encoding(): SqliteTextEncoding {
|
|
67
|
+
const name = this.#native.encoding();
|
|
68
|
+
if (name === "utf16le" || name === "utf16be" || name === "utf8") return name;
|
|
69
|
+
throw new Error(`Unexpected SQLite encoding label: ${name}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Health probe re-running the full main-file validation. Returns 1 on success. */
|
|
73
|
+
ping(): number {
|
|
74
|
+
return this.#native.ping();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Raw main-file bytes, including the page-size header. */
|
|
78
|
+
pages(): Buffer {
|
|
79
|
+
return this.#native.pages();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* File-backed or blank in-memory SQLite snapshot handle.
|
|
85
|
+
*
|
|
86
|
+
* No WAL or journal recovery is performed and no SQL engine is present.
|
|
87
|
+
* Use this when the path and header meta matter beyond raw main-file bytes.
|
|
88
|
+
*/
|
|
89
|
+
export class SqliteDatabase {
|
|
90
|
+
readonly #native: SqliteDatabaseBinding;
|
|
91
|
+
|
|
92
|
+
private constructor(native: SqliteDatabaseBinding) {
|
|
93
|
+
this.#native = native;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Opens a blank in-memory SQLite format-3 snapshot. */
|
|
97
|
+
static openInMemory(): SqliteDatabase {
|
|
98
|
+
const native = loadYydsSqliteNative();
|
|
99
|
+
return new SqliteDatabase(native.SqliteDatabase.openInMemory());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Opens or creates a file-backed SQLite snapshot (main-file only). */
|
|
103
|
+
static open(path: string): SqliteDatabase {
|
|
104
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
105
|
+
throw new TypeError("SQLite path must be a non-empty string");
|
|
106
|
+
}
|
|
107
|
+
const native = loadYydsSqliteNative();
|
|
108
|
+
return new SqliteDatabase(native.SqliteDatabase.open(path));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** File path when file-backed, `null` for in-memory. */
|
|
112
|
+
path(): string | null {
|
|
113
|
+
return this.#native.path();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Health probe re-running the full main-file validation. Returns 1 on success. */
|
|
117
|
+
ping(): number {
|
|
118
|
+
return this.#native.ping();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** SQLite library version stamped in the snapshot header. */
|
|
122
|
+
sqliteVersion(): string {
|
|
123
|
+
return this.#native.sqliteVersion();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Returns the header-claimed text encoding. */
|
|
127
|
+
encoding(): SqliteTextEncoding {
|
|
128
|
+
const name = this.#native.encoding();
|
|
129
|
+
if (name === "utf16le" || name === "utf16be" || name === "utf8") return name;
|
|
130
|
+
throw new Error(`Unexpected SQLite encoding label: ${name}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
schema(limits?: SqliteReadLimits): SqliteSchemaObject[] {
|
|
134
|
+
return this.#native.schema(validateLimits(limits));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Reads rows with decoded storage-class values from an ordinary rowid table. */
|
|
138
|
+
table(name: string, limits?: SqliteReadLimits): SqliteDecodedRow[] {
|
|
139
|
+
if (typeof name !== "string") {
|
|
140
|
+
throw new TypeError("SQLite table name must be a string");
|
|
141
|
+
}
|
|
142
|
+
return this.#native.table(name, validateLimits(limits));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Reads raw rows (undecoded payload) from an ordinary rowid table. */
|
|
146
|
+
tableRaw(name: string, limits?: SqliteReadLimits): SqliteTableRow[] {
|
|
147
|
+
if (typeof name !== "string") {
|
|
148
|
+
throw new TypeError("SQLite table name must be a string");
|
|
149
|
+
}
|
|
150
|
+
return this.#native.tableRaw(name, validateLimits(limits));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Raw main-file bytes, including the page-size header. */
|
|
154
|
+
pages(): Buffer {
|
|
155
|
+
return this.#native.pages();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Decodes one SQLite record payload into typed storage-class values.
|
|
161
|
+
*
|
|
162
|
+
* Defaults to `utf8`. Feed the database header encoding (see `encoding()`)
|
|
163
|
+
* when the snapshot advertises utf16le or utf16be.
|
|
164
|
+
*/
|
|
165
|
+
export function decodeRecord(
|
|
166
|
+
payload: Uint8Array,
|
|
167
|
+
encoding?: SqliteTextEncoding,
|
|
168
|
+
): SqliteRecordValue[] {
|
|
169
|
+
if (!(payload instanceof Uint8Array)) {
|
|
170
|
+
throw new TypeError("payload must be a Uint8Array or Buffer");
|
|
171
|
+
}
|
|
172
|
+
const native = loadYydsSqliteNative();
|
|
173
|
+
const encArg: string | undefined =
|
|
174
|
+
encoding === undefined ? undefined : encoding;
|
|
175
|
+
return native.decodeRecord(Buffer.from(payload), encArg);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function unsignedLimit(value: number, name: string): void {
|
|
179
|
+
if (!Number.isInteger(value) || value < 0 || value > 0xffff_ffff) {
|
|
180
|
+
throw new RangeError(`${name} must be an unsigned 32-bit integer`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function validateLimits(limits?: SqliteReadLimits): SqliteReadLimits | undefined {
|
|
185
|
+
if (limits === undefined) return undefined;
|
|
186
|
+
if (limits === null || typeof limits !== "object" || Array.isArray(limits)) {
|
|
187
|
+
throw new TypeError("SQLite scan limits must be an object");
|
|
188
|
+
}
|
|
189
|
+
const keys = new Set(["maxPages", "maxRows", "maxPayloadBytes", "maxTotalPayloadBytes"]);
|
|
190
|
+
for (const [key, value] of Object.entries(limits)) {
|
|
191
|
+
if (!keys.has(key)) throw new TypeError(`Unknown SQLite scan limit: ${key}`);
|
|
192
|
+
if (value !== undefined) unsignedLimit(value, key);
|
|
193
|
+
}
|
|
194
|
+
return limits;
|
|
195
|
+
}
|
package/README.md
DELETED