ddlseed 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.
- package/LICENSE +21 -0
- package/README.md +163 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +168 -0
- package/dist/generator.d.ts +18 -0
- package/dist/generator.js +381 -0
- package/dist/graph.d.ts +7 -0
- package/dist/graph.js +47 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/parser.d.ts +3 -0
- package/dist/parser.js +637 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.js +2 -0
- package/dist/writers.d.ts +10 -0
- package/dist/writers.js +127 -0
- package/package.json +49 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ddlseed contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,164 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ddlseed
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/brianmojena/ddlseed/actions/workflows/ci.yml)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
**Deterministic, FK-consistent test data from your SQL schema. One command, zero config, fully offline.**
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
ddlseed --schema schema.sql --rows 100 --seed 42 --format sql > seed.sql
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## The problem it solves
|
|
14
|
+
|
|
15
|
+
Populating a development database or CI pipeline with test data is a chore:
|
|
16
|
+
|
|
17
|
+
- **Faker-style libraries** generate realistic values *per column* but leave it to you to
|
|
18
|
+
keep foreign keys, unique constraints and primary keys consistent across tables.
|
|
19
|
+
- **Snaplet/seed** (the popular schema-aware generator) needs a *live database connection*
|
|
20
|
+
and writes into it — no way to work offline from a schema dump.
|
|
21
|
+
- **Neosync** is a full platform (Docker, Temporal, Postgres) — heavy infrastructure for a seed file.
|
|
22
|
+
- **Commercial tools** (Seedfast, Mockaroo, Tonic…) are paid SaaS with row limits.
|
|
23
|
+
|
|
24
|
+
`ddlseed` is the missing middle ground: a single CLI that reads **just your DDL**
|
|
25
|
+
(`CREATE TABLE` statements — no database connection, no server, no accounts) and emits
|
|
26
|
+
**ready-to-load SQL INSERTs, JSON or CSV**, with foreign keys, primary keys, unique
|
|
27
|
+
constraints and NOT NULL respected by construction. Same seed → same data, every time.
|
|
28
|
+
|
|
29
|
+
## Who it's for
|
|
30
|
+
|
|
31
|
+
- Backend developers who need realistic local/CI fixtures for a relational schema.
|
|
32
|
+
- Teams that want reproducible seed data in code review (`--seed 42` diffs cleanly).
|
|
33
|
+
- Anyone who has a `.sql` schema dump and no running database (offline, air-gapped, new laptop).
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
Requires Node.js ≥ 18. The only runtime dependency is `@faker-js/faker` (MIT).
|
|
38
|
+
|
|
39
|
+
**From source** (the package is not yet published to npm):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
git clone https://github.com/brianmojena/ddlseed.git ddlseed
|
|
43
|
+
cd ddlseed
|
|
44
|
+
npm install
|
|
45
|
+
npm run build
|
|
46
|
+
node dist/cli.js --help # run it
|
|
47
|
+
npm link # optional: exposes `ddlseed` on your PATH
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Once published**, it will be simply:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm install -g ddlseed
|
|
54
|
+
# or run without installing:
|
|
55
|
+
npx ddlseed --help
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
ddlseed --schema <file.sql> [options]
|
|
62
|
+
|
|
63
|
+
Options:
|
|
64
|
+
-s, --schema <file> SQL DDL file with CREATE TABLE statements (required)
|
|
65
|
+
-n, --rows <n> Rows per table (default: 50)
|
|
66
|
+
--seed <n> Seed for deterministic output (default: 42)
|
|
67
|
+
-f, --format <fmt> sql | json | csv | all (default: sql)
|
|
68
|
+
-o, --out <dir> Output directory (default: stdout)
|
|
69
|
+
--dialect <d> sqlite | postgres | mysql | auto (default: auto)
|
|
70
|
+
--ref-date <date> Anchor date for timestamps, ISO 8601 (default: 2026-01-01)
|
|
71
|
+
-h, --help Show help
|
|
72
|
+
-v, --version Show version
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Examples
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# 100 rows per table as SQL, written to ./seed (seed.sql + data.json + <table>.csv)
|
|
79
|
+
ddlseed --schema schema.sql --rows 100 --seed 7 --format all --out ./seed
|
|
80
|
+
|
|
81
|
+
# SQL to stdout, pipe it straight into psql / sqlite3 / mysql
|
|
82
|
+
ddlseed --schema schema.sql --rows 200 | sqlite3 dev.db
|
|
83
|
+
|
|
84
|
+
# JSON for a REST mock server
|
|
85
|
+
ddlseed --schema schema.sql --format json > fixtures.json
|
|
86
|
+
|
|
87
|
+
# CSV per table for spreadsheets / BI tools
|
|
88
|
+
ddlseed --schema schema.sql --format csv --out ./csv
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### What it understands
|
|
92
|
+
|
|
93
|
+
- Dialects: **SQLite**, **PostgreSQL** and **MySQL** DDL (auto-detected, override with `--dialect`).
|
|
94
|
+
- `CREATE TABLE` with column-level and table-level constraints:
|
|
95
|
+
`PRIMARY KEY` (single and composite), `FOREIGN KEY … REFERENCES` (with
|
|
96
|
+
`ON DELETE/UPDATE` actions), `UNIQUE`, `NOT NULL`, `DEFAULT`, `CHECK` (ignored),
|
|
97
|
+
`AUTO_INCREMENT` / `AUTOINCREMENT` / `SERIAL` / `GENERATED … AS IDENTITY`,
|
|
98
|
+
`ENUM('a','b')` and quoted identifiers (`"x"`, `` `x` ``, `[x]`).
|
|
99
|
+
- Comments and other statements (`CREATE INDEX`, `INSERT`, …) are skipped.
|
|
100
|
+
- Self-referencing tables (e.g. `parent_id REFERENCES same_table(id)`) are handled.
|
|
101
|
+
|
|
102
|
+
### How values are generated
|
|
103
|
+
|
|
104
|
+
Column values are chosen by a name-then-type heuristic: a column called `email`
|
|
105
|
+
gets a realistic email, `first_name` a person's first name, `price` a decimal,
|
|
106
|
+
`created_at` a recent timestamp; everything else falls back to its SQL type.
|
|
107
|
+
UUIDs, JSON columns, ENUM choices, VARCHAR length limits and NULL probability
|
|
108
|
+
(~10 % on nullable columns) are all respected. Timestamps are anchored to a fixed
|
|
109
|
+
reference date so that the **same seed always produces byte-identical output**.
|
|
110
|
+
|
|
111
|
+
Output tables are emitted in foreign-key order, wrapped in a transaction —
|
|
112
|
+
the generated `seed.sql` loads cleanly with `PRAGMA foreign_keys = ON`.
|
|
113
|
+
|
|
114
|
+
## Demo
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
npm install
|
|
118
|
+
npm run demo
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
generates data for the example blog schema in [`examples/blog.sql`](examples/blog.sql)
|
|
122
|
+
into `demo-out/` and prints a preview. Then try loading it for real:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
sqlite3 test.db < examples/blog.sql # SQLite needs JSON→TEXT: see scripts/demo.mjs
|
|
126
|
+
sqlite3 test.db < demo-out/seed.sql
|
|
127
|
+
sqlite3 test.db "SELECT COUNT(*) FROM posts;"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Development
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npm install # install dependencies
|
|
134
|
+
npm run build # compile TypeScript to dist/
|
|
135
|
+
npm test # build + run the full test suite (vitest)
|
|
136
|
+
npm run demo # build + reproducible demo
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The test suite (53 tests) covers the parser, dependency ordering, generator
|
|
140
|
+
(FK consistency, uniqueness, determinism), writers, the CLI end-to-end, and an
|
|
141
|
+
**integration test that loads the generated SQL into a real SQLite database**
|
|
142
|
+
(via `node:sqlite`) and asserts zero orphan rows and zero duplicate keys.
|
|
143
|
+
|
|
144
|
+
## How it compares
|
|
145
|
+
|
|
146
|
+
| Tool | Works from DDL only (offline) | FK-consistent | Emits SQL/JSON/CSV files | Deterministic | Free & open source | No server |
|
|
147
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
148
|
+
| **ddlseed** | ✅ | ✅ | ✅ | ✅ | ✅ MIT | ✅ |
|
|
149
|
+
| faker.js / Mimesis | ❌ (column-level only) | ❌ manual | ❌ you write it | ✅ | ✅ | ✅ |
|
|
150
|
+
| supabase-community/seed | ❌ needs live DB | ✅ | ❌ writes into the DB | ✅ | ✅ | ❌ |
|
|
151
|
+
| shuttle-hq/synth | ❌ handwritten configs | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
152
|
+
| Neosync | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ Docker+Temporal |
|
|
153
|
+
| Benerator CE | ❌ JDBC + XML | ✅ | ✅ | ✅ | ⚠️ Java+XML | ⚠️ |
|
|
154
|
+
| Seedfast / Mockaroo / Tonic | ⚠️ | ✅ | ✅ | ✅ | ❌ paid SaaS | ❌ |
|
|
155
|
+
|
|
156
|
+
*Table reflects capabilities as verified in November 2026 — see `docs/DECISION.md` for the full research log.*
|
|
157
|
+
|
|
158
|
+
## Contributing
|
|
159
|
+
|
|
160
|
+
Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
[MIT](LICENSE) — free forever, no accounts, no telemetry, no network calls.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { generateData } from './generator.js';
|
|
5
|
+
import { parseSchema } from './parser.js';
|
|
6
|
+
import { DdlSeedError } from './types.js';
|
|
7
|
+
import { generationOrder } from './generator.js';
|
|
8
|
+
import { toCsv, toCsvAll, toJson, toSql } from './writers.js';
|
|
9
|
+
const VERSION = '0.1.0';
|
|
10
|
+
function usage() {
|
|
11
|
+
return `ddlseed ${VERSION} — deterministic, FK-consistent test data from your SQL schema.
|
|
12
|
+
|
|
13
|
+
USAGE
|
|
14
|
+
ddlseed --schema <file.sql> [options]
|
|
15
|
+
|
|
16
|
+
OPTIONS
|
|
17
|
+
-s, --schema <file> SQL DDL file with CREATE TABLE statements (required)
|
|
18
|
+
-n, --rows <n> Rows per table (default: 50)
|
|
19
|
+
--seed <n> Seed for deterministic output (default: 42)
|
|
20
|
+
-f, --format <fmt> sql | json | csv | all (default: sql)
|
|
21
|
+
-o, --out <dir> Output directory (default: stdout)
|
|
22
|
+
--dialect <d> sqlite | postgres | mysql | auto (default: auto)
|
|
23
|
+
--ref-date <date> Anchor date for timestamps, ISO 8601 (default: 2026-01-01)
|
|
24
|
+
-h, --help Show this help
|
|
25
|
+
-v, --version Show version
|
|
26
|
+
|
|
27
|
+
EXAMPLES
|
|
28
|
+
ddlseed --schema schema.sql --rows 100 --seed 7 --format all --out ./seed
|
|
29
|
+
ddlseed --schema schema.sql --format sql > seed.sql
|
|
30
|
+
|
|
31
|
+
Output (with --out): seed.sql, data.json, <table>.csv per table.
|
|
32
|
+
Without --out: the chosen format is printed to stdout.`;
|
|
33
|
+
}
|
|
34
|
+
function parseArgs(argv) {
|
|
35
|
+
const args = { rows: 50, seed: 42, format: 'sql', help: false, version: false };
|
|
36
|
+
for (let i = 0; i < argv.length; i++) {
|
|
37
|
+
const a = argv[i];
|
|
38
|
+
const next = () => {
|
|
39
|
+
if (i + 1 >= argv.length)
|
|
40
|
+
throw new DdlSeedError(`Missing value for ${a}`);
|
|
41
|
+
return argv[++i];
|
|
42
|
+
};
|
|
43
|
+
switch (a) {
|
|
44
|
+
case '-s':
|
|
45
|
+
case '--schema':
|
|
46
|
+
args.schema = next();
|
|
47
|
+
break;
|
|
48
|
+
case '-n':
|
|
49
|
+
case '--rows': {
|
|
50
|
+
const v = Number(next());
|
|
51
|
+
if (!Number.isInteger(v) || v < 0)
|
|
52
|
+
throw new DdlSeedError(`--rows must be a non-negative integer`);
|
|
53
|
+
args.rows = v;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case '--seed': {
|
|
57
|
+
const v = Number(next());
|
|
58
|
+
if (!Number.isInteger(v))
|
|
59
|
+
throw new DdlSeedError(`--seed must be an integer`);
|
|
60
|
+
args.seed = v;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case '-f':
|
|
64
|
+
case '--format': {
|
|
65
|
+
const v = next();
|
|
66
|
+
if (!['sql', 'json', 'csv', 'all'].includes(v))
|
|
67
|
+
throw new DdlSeedError(`--format must be sql, json, csv or all`);
|
|
68
|
+
args.format = v;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case '-o':
|
|
72
|
+
case '--out':
|
|
73
|
+
args.out = next();
|
|
74
|
+
break;
|
|
75
|
+
case '--dialect': {
|
|
76
|
+
const v = next();
|
|
77
|
+
if (!['sqlite', 'postgres', 'mysql', 'auto'].includes(v))
|
|
78
|
+
throw new DdlSeedError(`--dialect must be sqlite, postgres, mysql or auto`);
|
|
79
|
+
args.dialect = v === 'auto' ? undefined : v;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case '--ref-date':
|
|
83
|
+
args.refDate = next();
|
|
84
|
+
break;
|
|
85
|
+
case '-h':
|
|
86
|
+
case '--help':
|
|
87
|
+
args.help = true;
|
|
88
|
+
break;
|
|
89
|
+
case '-v':
|
|
90
|
+
case '--version':
|
|
91
|
+
args.version = true;
|
|
92
|
+
break;
|
|
93
|
+
default: throw new DdlSeedError(`Unknown option: ${a}\n\n${usage()}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return args;
|
|
97
|
+
}
|
|
98
|
+
function run(args) {
|
|
99
|
+
if (args.version) {
|
|
100
|
+
process.stdout.write(`ddlseed ${VERSION}\n`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (args.help) {
|
|
104
|
+
process.stdout.write(usage() + '\n');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!args.schema) {
|
|
108
|
+
throw new DdlSeedError(`--schema is required.\n\n${usage()}`);
|
|
109
|
+
}
|
|
110
|
+
const sql = fs.readFileSync(args.schema, 'utf8');
|
|
111
|
+
const schema = parseSchema(sql, args.dialect);
|
|
112
|
+
if (schema.tables.length === 0) {
|
|
113
|
+
throw new DdlSeedError(`No CREATE TABLE statements found in "${args.schema}".`);
|
|
114
|
+
}
|
|
115
|
+
const data = generateData(schema, {
|
|
116
|
+
rows: args.rows,
|
|
117
|
+
seed: args.seed,
|
|
118
|
+
refDate: args.refDate,
|
|
119
|
+
});
|
|
120
|
+
if (args.out) {
|
|
121
|
+
fs.mkdirSync(args.out, { recursive: true });
|
|
122
|
+
const written = [];
|
|
123
|
+
if (args.format === 'sql' || args.format === 'all') {
|
|
124
|
+
const p = path.join(args.out, 'seed.sql');
|
|
125
|
+
fs.writeFileSync(p, toSql(schema, data, schema.dialect));
|
|
126
|
+
written.push(p);
|
|
127
|
+
}
|
|
128
|
+
if (args.format === 'json' || args.format === 'all') {
|
|
129
|
+
const p = path.join(args.out, 'data.json');
|
|
130
|
+
fs.writeFileSync(p, toJson(schema, data));
|
|
131
|
+
written.push(p);
|
|
132
|
+
}
|
|
133
|
+
if (args.format === 'csv' || args.format === 'all') {
|
|
134
|
+
for (const t of generationOrder(schema)) {
|
|
135
|
+
const p = path.join(args.out, `${t.name}.csv`);
|
|
136
|
+
fs.writeFileSync(p, toCsv(schema, data, t));
|
|
137
|
+
written.push(p);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
process.stderr.write(`ddlseed: ${schema.tables.length} tables, ${args.rows} rows/table ->\n`);
|
|
141
|
+
for (const p of written)
|
|
142
|
+
process.stderr.write(` ${p}\n`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// stdout output
|
|
146
|
+
if (args.format === 'sql') {
|
|
147
|
+
process.stdout.write(toSql(schema, data, schema.dialect));
|
|
148
|
+
}
|
|
149
|
+
else if (args.format === 'json') {
|
|
150
|
+
process.stdout.write(toJson(schema, data));
|
|
151
|
+
}
|
|
152
|
+
else if (args.format === 'csv') {
|
|
153
|
+
process.stdout.write(toCsvAll(schema, data));
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
process.stdout.write(toSql(schema, data, schema.dialect));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
run(parseArgs(process.argv.slice(2)));
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
const msg = err instanceof DdlSeedError
|
|
164
|
+
? err.message
|
|
165
|
+
: err instanceof Error ? err.message : String(err);
|
|
166
|
+
process.stderr.write(`ddlseed: error: ${msg}\n`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Dialect, Row, Schema, TableDef } from './types.js';
|
|
2
|
+
export interface GenerateOptions {
|
|
3
|
+
/** Rows per table. */
|
|
4
|
+
rows?: number;
|
|
5
|
+
/** Seed for deterministic output. Same seed => same data. */
|
|
6
|
+
seed?: number;
|
|
7
|
+
/** Anchor date for generated timestamps (determinism). Default: fixed 2026-01-01. */
|
|
8
|
+
refDate?: Date | string;
|
|
9
|
+
}
|
|
10
|
+
interface ResolvedFk {
|
|
11
|
+
columns: string[];
|
|
12
|
+
refTable: TableDef;
|
|
13
|
+
refColumns: string[];
|
|
14
|
+
}
|
|
15
|
+
export declare function validateSchema(schema: Schema): Map<TableDef, ResolvedFk[]>;
|
|
16
|
+
export declare function generateData(schema: Schema, options?: GenerateOptions): Map<string, Row[]>;
|
|
17
|
+
export declare function generationOrder(schema: Schema): TableDef[];
|
|
18
|
+
export type { Dialect };
|