@sqldoc/cli 0.0.1

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/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "type": "module",
3
+ "name": "@sqldoc/cli",
4
+ "version": "0.0.1",
5
+ "description": "CLI tool for sqldoc -- compile and validate SQL files with tag-driven generation",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./src/index.ts",
9
+ "import": "./src/index.ts",
10
+ "default": "./src/index.ts"
11
+ }
12
+ },
13
+ "main": "./src/index.ts",
14
+ "types": "./src/index.ts",
15
+ "bin": {
16
+ "sqldoc": "./src/index.ts"
17
+ },
18
+ "files": [
19
+ "src",
20
+ "package.json"
21
+ ],
22
+ "dependencies": {
23
+ "commander": "^14.0.3",
24
+ "fast-glob": "^3.3.3",
25
+ "picocolors": "^1.1.1",
26
+ "@sqldoc/atlas": "0.0.1",
27
+ "@sqldoc/core": "0.0.1"
28
+ },
29
+ "devDependencies": {
30
+ "typescript": "^5.9.3",
31
+ "vitest": "^4.1.0"
32
+ },
33
+ "scripts": {
34
+ "test": "vitest run",
35
+ "typecheck": "tsc --noEmit"
36
+ }
37
+ }
@@ -0,0 +1,19 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { isBunRuntime, isCompiledBinary } from '../runtime'
3
+
4
+ describe('runtime detection', () => {
5
+ it('isBunRuntime returns false in Node.js', () => {
6
+ expect(isBunRuntime()).toBe(false)
7
+ })
8
+
9
+ it('isCompiledBinary returns false in Node.js', () => {
10
+ expect(isCompiledBinary()).toBe(false)
11
+ })
12
+
13
+ it('isCompiledBinary depends on isBunRuntime', () => {
14
+ // If not running in Bun, compiled binary detection must also be false
15
+ if (!isBunRuntime()) {
16
+ expect(isCompiledBinary()).toBe(false)
17
+ }
18
+ })
19
+ })
@@ -0,0 +1,103 @@
1
+ import * as fs from 'node:fs'
2
+ import * as os from 'node:os'
3
+ import * as path from 'node:path'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+
6
+ describe('codegenCommand', () => {
7
+ let tmpDir: string
8
+
9
+ beforeEach(() => {
10
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sqldoc-cli-codegen-'))
11
+ })
12
+
13
+ afterEach(() => {
14
+ fs.rmSync(tmpDir, { recursive: true, force: true })
15
+ })
16
+
17
+ it('does not write SQL to stdout', async () => {
18
+ const nsFile = path.join(tmpDir, 'ns-test.ts')
19
+ fs.writeFileSync(
20
+ nsFile,
21
+ `
22
+ export default {
23
+ name: 'test',
24
+ tags: {
25
+ marker: { description: 'test marker', targets: ['table'] },
26
+ },
27
+ apiVersion: 1,
28
+ generateSQL(ctx) {
29
+ return [{ sql: '-- generated by test namespace' }]
30
+ },
31
+ }
32
+ `,
33
+ 'utf-8',
34
+ )
35
+
36
+ const sqlFile = path.join(tmpDir, 'schema.sql')
37
+ fs.writeFileSync(
38
+ sqlFile,
39
+ `-- @import './ns-test.ts'
40
+ -- @test.marker
41
+ CREATE TABLE users (
42
+ id bigserial PRIMARY KEY
43
+ );
44
+ `,
45
+ 'utf-8',
46
+ )
47
+
48
+ const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true)
49
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
50
+
51
+ const { codegenCommand } = await import('../commands/codegen.ts')
52
+ await codegenCommand(sqlFile, {})
53
+
54
+ // codegen should NOT write SQL to stdout (no -o option exists)
55
+ const stdoutOutput = writeSpy.mock.calls.map((c) => String(c[0])).join('')
56
+ expect(stdoutOutput).not.toContain('CREATE TABLE users')
57
+
58
+ consoleErrorSpy.mockRestore()
59
+ writeSpy.mockRestore()
60
+ }, 30_000)
61
+
62
+ it('throws CliError when errors are encountered', async () => {
63
+ const nsFile = path.join(tmpDir, 'ns-error.ts')
64
+ fs.writeFileSync(
65
+ nsFile,
66
+ `
67
+ export default {
68
+ name: 'broken',
69
+ tags: {
70
+ fail: { description: 'always fails', targets: ['table'] },
71
+ },
72
+ apiVersion: 1,
73
+ generateSQL(ctx) {
74
+ throw new Error('Generator exploded')
75
+ },
76
+ }
77
+ `,
78
+ 'utf-8',
79
+ )
80
+
81
+ const sqlFile = path.join(tmpDir, 'broken.sql')
82
+ fs.writeFileSync(
83
+ sqlFile,
84
+ `-- @import './ns-error.ts'
85
+ -- @broken.fail
86
+ CREATE TABLE bad (
87
+ id bigserial PRIMARY KEY
88
+ );
89
+ `,
90
+ 'utf-8',
91
+ )
92
+
93
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
94
+ const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true)
95
+
96
+ const { codegenCommand } = await import('../commands/codegen.ts')
97
+ const { CliError } = await import('../errors.ts')
98
+ await expect(codegenCommand(sqlFile, {})).rejects.toThrow(CliError)
99
+
100
+ consoleErrorSpy.mockRestore()
101
+ writeSpy.mockRestore()
102
+ })
103
+ })
@@ -0,0 +1,132 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { detectDestructiveChanges } from '../utils/destructive.ts'
3
+
4
+ describe('detectDestructiveChanges', () => {
5
+ it('returns empty array for non-destructive statements', () => {
6
+ const statements = [
7
+ 'CREATE TABLE users (id INT)',
8
+ 'ALTER TABLE users ADD COLUMN email TEXT',
9
+ 'CREATE INDEX idx_users_email ON users (email)',
10
+ ]
11
+ const result = detectDestructiveChanges(statements)
12
+ expect(result).toEqual([])
13
+ })
14
+
15
+ it('detects DROP TABLE', () => {
16
+ const statements = ['DROP TABLE audit_log']
17
+ const result = detectDestructiveChanges(statements)
18
+ expect(result).toHaveLength(1)
19
+ expect(result[0].description).toBe('DROP TABLE audit_log')
20
+ })
21
+
22
+ it('detects DROP TABLE IF EXISTS', () => {
23
+ const statements = ['DROP TABLE IF EXISTS audit_log']
24
+ const result = detectDestructiveChanges(statements)
25
+ expect(result).toHaveLength(1)
26
+ expect(result[0].description).toBe('DROP TABLE audit_log')
27
+ })
28
+
29
+ it('detects DROP TABLE with quoted name', () => {
30
+ const statements = ['DROP TABLE "audit_log"']
31
+ const result = detectDestructiveChanges(statements)
32
+ expect(result).toHaveLength(1)
33
+ expect(result[0].description).toBe('DROP TABLE audit_log')
34
+ })
35
+
36
+ it('detects ALTER TABLE DROP COLUMN', () => {
37
+ const statements = ['ALTER TABLE users DROP COLUMN legacy_id']
38
+ const result = detectDestructiveChanges(statements)
39
+ expect(result).toHaveLength(1)
40
+ expect(result[0].description).toBe('ALTER TABLE users DROP COLUMN legacy_id')
41
+ })
42
+
43
+ it('detects ALTER TABLE DROP COLUMN IF EXISTS', () => {
44
+ const statements = ['ALTER TABLE users DROP COLUMN IF EXISTS legacy_id']
45
+ const result = detectDestructiveChanges(statements)
46
+ expect(result).toHaveLength(1)
47
+ expect(result[0].description).toBe('ALTER TABLE users DROP COLUMN legacy_id')
48
+ })
49
+
50
+ it('detects ALTER TABLE ONLY ... DROP COLUMN', () => {
51
+ const statements = ['ALTER TABLE ONLY users DROP COLUMN legacy_id']
52
+ const result = detectDestructiveChanges(statements)
53
+ expect(result).toHaveLength(1)
54
+ expect(result[0].description).toBe('ALTER TABLE users DROP COLUMN legacy_id')
55
+ })
56
+
57
+ it('detects DROP INDEX', () => {
58
+ const statements = ['DROP INDEX idx_users_email']
59
+ const result = detectDestructiveChanges(statements)
60
+ expect(result).toHaveLength(1)
61
+ expect(result[0].description).toBe('DROP INDEX idx_users_email')
62
+ })
63
+
64
+ it('detects DROP INDEX CONCURRENTLY IF EXISTS', () => {
65
+ const statements = ['DROP INDEX CONCURRENTLY IF EXISTS idx_users_email']
66
+ const result = detectDestructiveChanges(statements)
67
+ expect(result).toHaveLength(1)
68
+ expect(result[0].description).toBe('DROP INDEX idx_users_email')
69
+ })
70
+
71
+ it('detects DROP VIEW', () => {
72
+ const statements = ['DROP VIEW active_users']
73
+ const result = detectDestructiveChanges(statements)
74
+ expect(result).toHaveLength(1)
75
+ expect(result[0].description).toBe('DROP VIEW active_users')
76
+ })
77
+
78
+ it('detects DROP FUNCTION', () => {
79
+ const statements = ['DROP FUNCTION calculate_total']
80
+ const result = detectDestructiveChanges(statements)
81
+ expect(result).toHaveLength(1)
82
+ expect(result[0].description).toBe('DROP FUNCTION calculate_total')
83
+ })
84
+
85
+ it('detects TRUNCATE', () => {
86
+ const statements = ['TRUNCATE TABLE sessions']
87
+ const result = detectDestructiveChanges(statements)
88
+ expect(result).toHaveLength(1)
89
+ expect(result[0].description).toBe('TRUNCATE sessions')
90
+ })
91
+
92
+ it('detects TRUNCATE without TABLE keyword', () => {
93
+ const statements = ['TRUNCATE sessions']
94
+ const result = detectDestructiveChanges(statements)
95
+ expect(result).toHaveLength(1)
96
+ expect(result[0].description).toBe('TRUNCATE sessions')
97
+ })
98
+
99
+ it('detects multiple destructive changes', () => {
100
+ const statements = [
101
+ 'CREATE TABLE new_users (id INT)',
102
+ 'DROP TABLE audit_log',
103
+ 'ALTER TABLE users DROP COLUMN legacy_id',
104
+ 'CREATE INDEX idx ON new_users (id)',
105
+ 'DROP INDEX idx_old',
106
+ ]
107
+ const result = detectDestructiveChanges(statements)
108
+ expect(result).toHaveLength(3)
109
+ expect(result[0].description).toBe('DROP TABLE audit_log')
110
+ expect(result[1].description).toBe('ALTER TABLE users DROP COLUMN legacy_id')
111
+ expect(result[2].description).toBe('DROP INDEX idx_old')
112
+ })
113
+
114
+ it('is case-insensitive', () => {
115
+ const statements = ['drop table USERS', 'alter table Orders drop column old_col']
116
+ const result = detectDestructiveChanges(statements)
117
+ expect(result).toHaveLength(2)
118
+ })
119
+
120
+ it('handles multiline statements (whitespace normalization)', () => {
121
+ const statements = ['ALTER TABLE\n users\n DROP COLUMN\n legacy_id']
122
+ const result = detectDestructiveChanges(statements)
123
+ expect(result).toHaveLength(1)
124
+ expect(result[0].description).toBe('ALTER TABLE users DROP COLUMN legacy_id')
125
+ })
126
+
127
+ it('preserves the original statement in the result', () => {
128
+ const stmt = 'DROP TABLE "my_table"'
129
+ const result = detectDestructiveChanges([stmt])
130
+ expect(result[0].statement).toBe(stmt)
131
+ })
132
+ })