@sqldoc/templates 0.0.1 → 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.
Files changed (70) hide show
  1. package/package.json +10 -8
  2. package/src/__tests__/dedent.test.ts +0 -45
  3. package/src/__tests__/docker-templates.test.ts +0 -134
  4. package/src/__tests__/go-structs.test.ts +0 -184
  5. package/src/__tests__/naming.test.ts +0 -48
  6. package/src/__tests__/python-dataclasses.test.ts +0 -185
  7. package/src/__tests__/rust-structs.test.ts +0 -176
  8. package/src/__tests__/tags-helpers.test.ts +0 -72
  9. package/src/__tests__/type-mapping.test.ts +0 -332
  10. package/src/__tests__/typescript.test.ts +0 -202
  11. package/src/cobol-copybook/test/.gitignore +0 -6
  12. package/src/cobol-copybook/test/Dockerfile +0 -7
  13. package/src/csharp-records/test/.gitignore +0 -6
  14. package/src/csharp-records/test/Dockerfile +0 -6
  15. package/src/diesel/test/.gitignore +0 -6
  16. package/src/diesel/test/Dockerfile +0 -16
  17. package/src/drizzle/test/.gitignore +0 -6
  18. package/src/drizzle/test/Dockerfile +0 -8
  19. package/src/drizzle/test/test.ts +0 -71
  20. package/src/efcore/test/.gitignore +0 -6
  21. package/src/efcore/test/Dockerfile +0 -7
  22. package/src/go-structs/test/.gitignore +0 -6
  23. package/src/go-structs/test/Dockerfile +0 -13
  24. package/src/go-structs/test/test.go +0 -71
  25. package/src/gorm/test/.gitignore +0 -6
  26. package/src/gorm/test/Dockerfile +0 -13
  27. package/src/gorm/test/test.go +0 -65
  28. package/src/java-records/test/.gitignore +0 -6
  29. package/src/java-records/test/Dockerfile +0 -11
  30. package/src/java-records/test/Test.java +0 -93
  31. package/src/jpa/test/.gitignore +0 -6
  32. package/src/jpa/test/Dockerfile +0 -14
  33. package/src/jpa/test/Test.java +0 -111
  34. package/src/json-schema/test/.gitignore +0 -6
  35. package/src/json-schema/test/Dockerfile +0 -18
  36. package/src/knex/test/.gitignore +0 -6
  37. package/src/knex/test/Dockerfile +0 -7
  38. package/src/knex/test/test.ts +0 -75
  39. package/src/kotlin-data/test/.gitignore +0 -6
  40. package/src/kotlin-data/test/Dockerfile +0 -14
  41. package/src/kotlin-data/test/Test.kt +0 -82
  42. package/src/kysely/test/.gitignore +0 -6
  43. package/src/kysely/test/Dockerfile +0 -8
  44. package/src/kysely/test/test.ts +0 -82
  45. package/src/prisma/test/.gitignore +0 -6
  46. package/src/prisma/test/Dockerfile +0 -7
  47. package/src/protobuf/test/.gitignore +0 -6
  48. package/src/protobuf/test/Dockerfile +0 -6
  49. package/src/pydantic/test/.gitignore +0 -6
  50. package/src/pydantic/test/Dockerfile +0 -8
  51. package/src/pydantic/test/test.py +0 -63
  52. package/src/python-dataclasses/test/.gitignore +0 -6
  53. package/src/python-dataclasses/test/Dockerfile +0 -8
  54. package/src/python-dataclasses/test/test.py +0 -63
  55. package/src/rust-structs/test/.gitignore +0 -6
  56. package/src/rust-structs/test/Dockerfile +0 -22
  57. package/src/rust-structs/test/test.rs +0 -82
  58. package/src/sqlalchemy/test/.gitignore +0 -6
  59. package/src/sqlalchemy/test/Dockerfile +0 -8
  60. package/src/sqlalchemy/test/test.py +0 -61
  61. package/src/sqlc/test/.gitignore +0 -6
  62. package/src/sqlc/test/Dockerfile +0 -13
  63. package/src/sqlc/test/test.go +0 -91
  64. package/src/typescript/test/.gitignore +0 -6
  65. package/src/typescript/test/Dockerfile +0 -8
  66. package/src/typescript/test/test.ts +0 -89
  67. package/src/xsd/test/.gitignore +0 -6
  68. package/src/xsd/test/Dockerfile +0 -6
  69. package/src/zod/test/.gitignore +0 -6
  70. package/src/zod/test/Dockerfile +0 -6
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@sqldoc/templates",
4
- "version": "0.0.1",
4
+ "version": "0.0.2",
5
5
  "description": "Code generation templates for sqldoc -- 18 templates across 6 languages plus tagged template syntax helpers",
6
6
  "exports": {
7
7
  ".": {
@@ -134,12 +134,14 @@
134
134
  "types": "./src/index.ts",
135
135
  "files": [
136
136
  "src",
137
+ "!src/__tests__",
138
+ "!src/**/test",
137
139
  "package.json"
138
140
  ],
139
141
  "peerDependencies": {
140
- "@sqldoc/atlas": "0.0.1",
141
- "@sqldoc/core": "0.0.1",
142
- "@sqldoc/ns-codegen": "0.0.1"
142
+ "@sqldoc/atlas": "0.0.2",
143
+ "@sqldoc/ns-codegen": "0.0.2",
144
+ "@sqldoc/core": "0.0.2"
143
145
  },
144
146
  "devDependencies": {
145
147
  "@electric-sql/pglite": "^0.4.1",
@@ -149,10 +151,10 @@
149
151
  "tsx": "^4.21.0",
150
152
  "typescript": "^5.9.3",
151
153
  "vitest": "^4.1.0",
152
- "@sqldoc/atlas": "0.0.1",
153
- "@sqldoc/core": "0.0.1",
154
- "@sqldoc/ns-codegen": "0.0.1",
155
- "@sqldoc/ns-validate": "0.0.1"
154
+ "@sqldoc/core": "0.0.2",
155
+ "@sqldoc/ns-codegen": "0.0.2",
156
+ "@sqldoc/atlas": "0.0.2",
157
+ "@sqldoc/ns-validate": "0.0.2"
156
158
  },
157
159
  "scripts": {
158
160
  "test": "vitest run",
@@ -1,45 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { dedent } from '../tags/dedent'
3
-
4
- describe('dedent', () => {
5
- it('strips common leading whitespace', () => {
6
- const result = dedent`
7
- hello
8
- world
9
- `
10
- expect(result).toBe('hello\nworld')
11
- })
12
-
13
- it('preserves relative indentation', () => {
14
- const result = dedent`
15
- if (true) {
16
- return 1
17
- }
18
- `
19
- expect(result).toBe('if (true) {\n return 1\n}')
20
- })
21
-
22
- it('handles string interpolation', () => {
23
- const name = 'World'
24
- const result = dedent`
25
- hello ${name}
26
- goodbye
27
- `
28
- expect(result).toBe('hello World\ngoodbye')
29
- })
30
-
31
- it('handles zero-indent lines', () => {
32
- const result = dedent`
33
- hello
34
- world
35
- `
36
- expect(result).toBe('hello\nworld')
37
- })
38
-
39
- it('removes empty first and last lines', () => {
40
- const result = dedent`
41
- content
42
- `
43
- expect(result).toBe('content')
44
- })
45
- })
@@ -1,134 +0,0 @@
1
- /**
2
- * Docker-based template tests.
3
- *
4
- * Auto-discovers templates by finding src/<template>/test/Dockerfile.
5
- * Starts a Postgres container on a shared Docker network, seeds it,
6
- * runs codegen (output goes directly into each test dir), then for each template:
7
- * 1. docker build (typecheck / compile)
8
- * 2. docker run (integration test or no-op CMD)
9
- *
10
- * Run with: pnpm test:docker
11
- */
12
-
13
- import { execSync } from 'node:child_process'
14
- import { existsSync, readdirSync, readFileSync } from 'node:fs'
15
- import { dirname, join, resolve } from 'node:path'
16
- import { fileURLToPath } from 'node:url'
17
- import pg from 'pg'
18
- import { afterAll, beforeAll, describe, test } from 'vitest'
19
-
20
- const thisDir = dirname(fileURLToPath(import.meta.url))
21
- const TEMPLATES_DIR = resolve(thisDir, '../..')
22
- const SRC_DIR = join(TEMPLATES_DIR, 'src')
23
- const TEST_DIR = join(TEMPLATES_DIR, 'test')
24
- const REPO_ROOT = resolve(TEMPLATES_DIR, '../..')
25
-
26
- const NETWORK = 'sqldoc-test-net'
27
- const PG_CONTAINER = 'sqldoc-test-pg'
28
- const PG_PORT = 54321
29
- const DB_URL = `postgresql://postgres:postgres@${PG_CONTAINER}:5432/postgres`
30
-
31
- function discoverTemplates(): string[] {
32
- return readdirSync(SRC_DIR, { withFileTypes: true })
33
- .filter((d) => d.isDirectory() && existsSync(join(SRC_DIR, d.name, 'test', 'Dockerfile')))
34
- .map((d) => d.name)
35
- .sort()
36
- }
37
-
38
- const templates = discoverTemplates()
39
-
40
- function dockerExec(cmd: string, timeout = 30_000) {
41
- return execSync(cmd, { stdio: 'pipe', timeout }).toString()
42
- }
43
-
44
- beforeAll(async () => {
45
- // Clean up any leftovers from previous runs, then create fresh
46
- try {
47
- dockerExec(`docker rm -f ${PG_CONTAINER}`)
48
- } catch {}
49
- try {
50
- dockerExec(`docker network rm ${NETWORK}`)
51
- } catch {}
52
- dockerExec(`docker network create ${NETWORK}`)
53
- dockerExec(
54
- `docker run -d --name ${PG_CONTAINER} --network ${NETWORK} -p ${PG_PORT}:5432 -e POSTGRES_PASSWORD=postgres postgres:17-alpine`,
55
- )
56
-
57
- // Wait for postgres to be ready
58
- for (let i = 0; i < 30; i++) {
59
- try {
60
- dockerExec(`docker exec ${PG_CONTAINER} pg_isready -U postgres`)
61
- break
62
- } catch {
63
- await new Promise((r) => setTimeout(r, 1000))
64
- }
65
- }
66
-
67
- // Seed the database
68
- const client = new pg.Client({
69
- host: '127.0.0.1',
70
- port: PG_PORT,
71
- database: 'postgres',
72
- user: 'postgres',
73
- password: 'postgres',
74
- connectionTimeoutMillis: 10_000,
75
- })
76
- await client.connect()
77
- const schemaSql = readFileSync(join(TEST_DIR, 'fixture.sql'), 'utf-8')
78
- .split('\n')
79
- .filter((line) => !line.trim().startsWith('-- @import'))
80
- .join('\n')
81
- await client.query(schemaSql)
82
- await client.query(readFileSync(join(TEST_DIR, 'fixture-seed.sql'), 'utf-8'))
83
- await client.end()
84
- console.log('Database seeded.')
85
-
86
- // Run codegen — outputs directly into each src/<template>/test/ directory
87
- console.log('Running codegen...')
88
- execSync(
89
- `node ${join(REPO_ROOT, 'packages/cli/src/index.ts')} codegen -c ${join(TEST_DIR, 'sqldoc.config.ts')} ${join(TEST_DIR, 'fixture.sql')}`,
90
- {
91
- cwd: TEMPLATES_DIR,
92
- stdio: 'pipe',
93
- timeout: 120_000,
94
- env: { ...process.env, SQLDOC_RESOLVE_FROM_LOCAL_PACKAGE: 'true' },
95
- },
96
- )
97
- console.log(`Codegen complete. Testing ${templates.length} templates.`)
98
- }, 180_000)
99
-
100
- afterAll(() => {
101
- try {
102
- dockerExec(`docker rm -f ${PG_CONTAINER}`)
103
- } catch {}
104
- try {
105
- dockerExec(`docker network rm ${NETWORK}`)
106
- } catch {}
107
- })
108
-
109
- describe('docker templates', () => {
110
- for (const name of templates) {
111
- test(name, () => {
112
- const testDir = join(SRC_DIR, name, 'test')
113
- const tag = `sqldoc-test-${name}`
114
- try {
115
- execSync(`docker build -t ${tag} ${testDir}`, {
116
- stdio: 'pipe',
117
- timeout: 120_000,
118
- })
119
- execSync(`docker run --rm --network ${NETWORK} -e DATABASE_URL="${DB_URL}" ${tag}`, {
120
- stdio: 'pipe',
121
- timeout: 30_000,
122
- })
123
- } catch (err: any) {
124
- const stderr = err.stderr?.toString() ?? ''
125
- const stdout = err.stdout?.toString() ?? ''
126
- throw new Error(`Docker test failed for ${name}:\n${stderr}\n${stdout}`)
127
- } finally {
128
- try {
129
- execSync(`docker rmi ${tag}`, { stdio: 'ignore' })
130
- } catch {}
131
- }
132
- }, 180_000)
133
- }
134
- })
@@ -1,184 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import goStructs from '../go-structs/index.ts'
3
-
4
- const generate = goStructs.generate
5
-
6
- import type { AtlasRealm } from '@sqldoc/atlas'
7
- import type { TemplateContext } from '@sqldoc/ns-codegen'
8
-
9
- const testRealm: AtlasRealm = {
10
- schemas: [
11
- {
12
- name: 'public',
13
- tables: [
14
- {
15
- name: 'users',
16
- columns: [
17
- { name: 'id', type: { T: 'bigserial', null: false, category: 'integer' } },
18
- { name: 'email', type: { T: 'character varying', raw: 'varchar(255)', null: false, category: 'string' } },
19
- { name: 'name', type: { T: 'text', null: true, category: 'string' } },
20
- { name: 'age', type: { T: 'integer', null: true, category: 'integer' } },
21
- { name: 'is_active', type: { T: 'boolean', null: false, category: 'boolean' } },
22
- { name: 'metadata', type: { T: 'jsonb', null: true, category: 'json' } },
23
- { name: 'created_at', type: { T: 'timestamp with time zone', null: false, category: 'time' } },
24
- { name: 'tags', type: { T: 'text[]', null: true, category: 'array' } },
25
- { name: 'avatar', type: { T: 'bytea', null: true, category: 'binary' } },
26
- { name: 'balance', type: { T: 'numeric(10,2)', null: true, category: 'decimal' } },
27
- { name: 'external_id', type: { T: 'uuid', null: true, category: 'uuid' } },
28
- ],
29
- primary_key: { parts: [{ column: 'id' }] },
30
- },
31
- {
32
- name: 'posts',
33
- columns: [
34
- { name: 'id', type: { T: 'bigserial', null: false, category: 'integer' } },
35
- { name: 'user_id', type: { T: 'bigint', null: false, category: 'integer' } },
36
- { name: 'title', type: { T: 'text', null: false, category: 'string' } },
37
- { name: 'body', type: { T: 'text', null: false, category: 'string' } },
38
- { name: 'published_at', type: { T: 'timestamp with time zone', null: true, category: 'time' } },
39
- { name: 'view_count', type: { T: 'integer', null: false, category: 'integer' } },
40
- { name: 'rating', type: { T: 'double precision', null: true, category: 'float' } },
41
- ],
42
- primary_key: { parts: [{ column: 'id' }] },
43
- foreign_keys: [
44
- { symbol: 'posts_user_id_fkey', columns: ['user_id'], ref_table: 'users', ref_columns: ['id'] },
45
- ],
46
- },
47
- {
48
- name: 'comments',
49
- columns: [
50
- { name: 'id', type: { T: 'bigserial', null: false, category: 'integer' } },
51
- { name: 'post_id', type: { T: 'bigint', null: false, category: 'integer' } },
52
- { name: 'user_id', type: { T: 'bigint', null: false, category: 'integer' } },
53
- { name: 'content', type: { T: 'text', null: false, category: 'string' } },
54
- { name: 'created_at', type: { T: 'timestamp with time zone', null: false, category: 'time' } },
55
- ],
56
- primary_key: { parts: [{ column: 'id' }] },
57
- foreign_keys: [
58
- { symbol: 'comments_post_id_fkey', columns: ['post_id'], ref_table: 'posts', ref_columns: ['id'] },
59
- { symbol: 'comments_user_id_fkey', columns: ['user_id'], ref_table: 'users', ref_columns: ['id'] },
60
- ],
61
- },
62
- ],
63
- },
64
- ],
65
- }
66
-
67
- function makeCtx(overrides?: Partial<TemplateContext>): TemplateContext {
68
- return {
69
- realm: testRealm,
70
- allFileTags: [],
71
- docsMeta: [],
72
- config: {},
73
- output: './out',
74
- templateName: 'go-structs',
75
- ...overrides,
76
- }
77
- }
78
-
79
- describe('go-structs template', () => {
80
- it('generates Go structs with json tags', () => {
81
- const result = generate(makeCtx())
82
- expect(result.files).toHaveLength(1)
83
- expect(result.files[0].path).toBe('models.go')
84
-
85
- const content = result.files[0].content
86
- expect(content).toContain('type Users struct {')
87
- expect(content).toContain('type Posts struct {')
88
- expect(content).toContain('type Comments struct {')
89
- })
90
-
91
- it('maps bigserial to int64', () => {
92
- const result = generate(makeCtx())
93
- const content = result.files[0].content
94
- expect(content).toContain('Id int64')
95
- })
96
-
97
- it('maps nullable text to *string', () => {
98
- const result = generate(makeCtx())
99
- const content = result.files[0].content
100
- expect(content).toContain('Name *string')
101
- })
102
-
103
- it('maps text[] to []string', () => {
104
- const result = generate(makeCtx())
105
- const content = result.files[0].content
106
- expect(content).toContain('Tags []string')
107
- })
108
-
109
- it('includes time import for timestamptz', () => {
110
- const result = generate(makeCtx())
111
- const content = result.files[0].content
112
- expect(content).toContain('import (')
113
- expect(content).toContain('"time"')
114
- })
115
-
116
- it('uses omitempty for nullable json tags', () => {
117
- const result = generate(makeCtx())
118
- const content = result.files[0].content
119
- expect(content).toContain('`json:"name,omitempty"`')
120
- expect(content).toContain('`json:"email"`')
121
- })
122
-
123
- it('respects @codegen.skip tag', () => {
124
- const ctx = makeCtx({
125
- allFileTags: [
126
- {
127
- sourceFile: 'test.sql',
128
- objects: [
129
- {
130
- objectName: 'comments',
131
- target: 'table',
132
- tags: [{ namespace: 'codegen', tag: 'skip', args: [] }],
133
- },
134
- ],
135
- },
136
- ],
137
- })
138
- const result = generate(ctx)
139
- const content = result.files[0].content
140
- expect(content).not.toContain('type Comments struct')
141
- expect(content).toContain('type Users struct')
142
- })
143
-
144
- it('respects @codegen.rename tag', () => {
145
- const ctx = makeCtx({
146
- allFileTags: [
147
- {
148
- sourceFile: 'test.sql',
149
- objects: [
150
- {
151
- objectName: 'users',
152
- target: 'table',
153
- tags: [{ namespace: 'codegen', tag: 'rename', args: ['Account'] }],
154
- },
155
- ],
156
- },
157
- ],
158
- })
159
- const result = generate(ctx)
160
- const content = result.files[0].content
161
- expect(content).toContain('type Account struct')
162
- expect(content).not.toContain('type Users struct')
163
- })
164
-
165
- it('respects @codegen.type override on column', () => {
166
- const ctx = makeCtx({
167
- allFileTags: [
168
- {
169
- sourceFile: 'test.sql',
170
- objects: [
171
- {
172
- objectName: 'users.metadata',
173
- target: 'column',
174
- tags: [{ namespace: 'codegen', tag: 'type', args: ['map[string]interface{}'] }],
175
- },
176
- ],
177
- },
178
- ],
179
- })
180
- const result = generate(ctx)
181
- const content = result.files[0].content
182
- expect(content).toContain('Metadata map[string]interface{}')
183
- })
184
- })
@@ -1,48 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { toCamelCase, toPascalCase, toScreamingSnake } from '../helpers/naming'
3
-
4
- describe('toPascalCase', () => {
5
- it('converts snake_case to PascalCase', () => {
6
- expect(toPascalCase('user_accounts')).toBe('UserAccounts')
7
- })
8
-
9
- it('handles single word', () => {
10
- expect(toPascalCase('id')).toBe('Id')
11
- })
12
-
13
- it('handles leading underscore', () => {
14
- expect(toPascalCase('_private_field')).toBe('PrivateField')
15
- })
16
-
17
- it('handles multiple underscores', () => {
18
- expect(toPascalCase('user__name')).toBe('UserName')
19
- })
20
-
21
- it('handles already capitalized segments', () => {
22
- expect(toPascalCase('USER_STATUS')).toBe('UserStatus')
23
- })
24
- })
25
-
26
- describe('toCamelCase', () => {
27
- it('converts snake_case to camelCase', () => {
28
- expect(toCamelCase('user_accounts')).toBe('userAccounts')
29
- })
30
-
31
- it('handles single word', () => {
32
- expect(toCamelCase('name')).toBe('name')
33
- })
34
-
35
- it('produces lowercase first char', () => {
36
- expect(toCamelCase('ID')).toBe('id')
37
- })
38
- })
39
-
40
- describe('toScreamingSnake', () => {
41
- it('converts to SCREAMING_SNAKE', () => {
42
- expect(toScreamingSnake('user_status')).toBe('USER_STATUS')
43
- })
44
-
45
- it('handles already lowercase', () => {
46
- expect(toScreamingSnake('created_at')).toBe('CREATED_AT')
47
- })
48
- })
@@ -1,185 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import pythonDataclasses from '../python-dataclasses/index.ts'
3
-
4
- const generate = pythonDataclasses.generate
5
-
6
- import type { AtlasRealm } from '@sqldoc/atlas'
7
- import type { TemplateContext } from '@sqldoc/ns-codegen'
8
-
9
- const testRealm: AtlasRealm = {
10
- schemas: [
11
- {
12
- name: 'public',
13
- tables: [
14
- {
15
- name: 'users',
16
- columns: [
17
- { name: 'id', type: { T: 'bigserial', null: false, category: 'integer' } },
18
- { name: 'email', type: { T: 'character varying', raw: 'varchar(255)', null: false, category: 'string' } },
19
- { name: 'name', type: { T: 'text', null: true, category: 'string' } },
20
- { name: 'age', type: { T: 'integer', null: true, category: 'integer' } },
21
- { name: 'is_active', type: { T: 'boolean', null: false, category: 'boolean' } },
22
- { name: 'metadata', type: { T: 'jsonb', null: true, category: 'json' } },
23
- { name: 'created_at', type: { T: 'timestamp with time zone', null: false, category: 'time' } },
24
- { name: 'tags', type: { T: 'text[]', null: true, category: 'array' } },
25
- { name: 'avatar', type: { T: 'bytea', null: true, category: 'binary' } },
26
- { name: 'balance', type: { T: 'numeric(10,2)', null: true, category: 'decimal' } },
27
- { name: 'external_id', type: { T: 'uuid', null: true, category: 'uuid' } },
28
- ],
29
- primary_key: { parts: [{ column: 'id' }] },
30
- },
31
- {
32
- name: 'posts',
33
- columns: [
34
- { name: 'id', type: { T: 'bigserial', null: false, category: 'integer' } },
35
- { name: 'user_id', type: { T: 'bigint', null: false, category: 'integer' } },
36
- { name: 'title', type: { T: 'text', null: false, category: 'string' } },
37
- { name: 'body', type: { T: 'text', null: false, category: 'string' } },
38
- { name: 'published_at', type: { T: 'timestamp with time zone', null: true, category: 'time' } },
39
- { name: 'view_count', type: { T: 'integer', null: false, category: 'integer' } },
40
- { name: 'rating', type: { T: 'double precision', null: true, category: 'float' } },
41
- ],
42
- primary_key: { parts: [{ column: 'id' }] },
43
- foreign_keys: [
44
- { symbol: 'posts_user_id_fkey', columns: ['user_id'], ref_table: 'users', ref_columns: ['id'] },
45
- ],
46
- },
47
- {
48
- name: 'comments',
49
- columns: [
50
- { name: 'id', type: { T: 'bigserial', null: false, category: 'integer' } },
51
- { name: 'post_id', type: { T: 'bigint', null: false, category: 'integer' } },
52
- { name: 'user_id', type: { T: 'bigint', null: false, category: 'integer' } },
53
- { name: 'content', type: { T: 'text', null: false, category: 'string' } },
54
- { name: 'created_at', type: { T: 'timestamp with time zone', null: false, category: 'time' } },
55
- ],
56
- primary_key: { parts: [{ column: 'id' }] },
57
- foreign_keys: [
58
- { symbol: 'comments_post_id_fkey', columns: ['post_id'], ref_table: 'posts', ref_columns: ['id'] },
59
- { symbol: 'comments_user_id_fkey', columns: ['user_id'], ref_table: 'users', ref_columns: ['id'] },
60
- ],
61
- },
62
- ],
63
- },
64
- ],
65
- }
66
-
67
- function makeCtx(overrides?: Partial<TemplateContext>): TemplateContext {
68
- return {
69
- realm: testRealm,
70
- allFileTags: [],
71
- docsMeta: [],
72
- config: {},
73
- output: './out',
74
- templateName: 'python-dataclasses',
75
- ...overrides,
76
- }
77
- }
78
-
79
- describe('python-dataclasses template', () => {
80
- it('generates dataclass definitions', () => {
81
- const result = generate(makeCtx())
82
- expect(result.files).toHaveLength(1)
83
- expect(result.files[0].path).toBe('models.py')
84
-
85
- const content = result.files[0].content
86
- expect(content).toContain('@dataclass')
87
- expect(content).toContain('class Users:')
88
- expect(content).toContain('class Posts:')
89
- expect(content).toContain('class Comments:')
90
- })
91
-
92
- it('maps bigserial to int', () => {
93
- const result = generate(makeCtx())
94
- const content = result.files[0].content
95
- expect(content).toContain('id: int')
96
- })
97
-
98
- it('maps nullable text to Optional[str]', () => {
99
- const result = generate(makeCtx())
100
- const content = result.files[0].content
101
- expect(content).toContain('name: Optional[str]')
102
- })
103
-
104
- it('maps nullable text[] to Optional[list[str]]', () => {
105
- const result = generate(makeCtx())
106
- const content = result.files[0].content
107
- expect(content).toContain('tags: Optional[list[str]]')
108
- })
109
-
110
- it('includes proper Python imports', () => {
111
- const result = generate(makeCtx())
112
- const content = result.files[0].content
113
- expect(content).toContain('from dataclasses import dataclass')
114
- expect(content).toContain('from typing import Optional')
115
- expect(content).toContain('from datetime import datetime')
116
- })
117
-
118
- it('nullable fields have = None default', () => {
119
- const result = generate(makeCtx())
120
- const content = result.files[0].content
121
- expect(content).toContain('name: Optional[str] = None')
122
- })
123
-
124
- it('respects @codegen.skip tag', () => {
125
- const ctx = makeCtx({
126
- allFileTags: [
127
- {
128
- sourceFile: 'test.sql',
129
- objects: [
130
- {
131
- objectName: 'comments',
132
- target: 'table',
133
- tags: [{ namespace: 'codegen', tag: 'skip', args: [] }],
134
- },
135
- ],
136
- },
137
- ],
138
- })
139
- const result = generate(ctx)
140
- const content = result.files[0].content
141
- expect(content).not.toContain('class Comments:')
142
- expect(content).toContain('class Users:')
143
- })
144
-
145
- it('respects @codegen.rename tag', () => {
146
- const ctx = makeCtx({
147
- allFileTags: [
148
- {
149
- sourceFile: 'test.sql',
150
- objects: [
151
- {
152
- objectName: 'users',
153
- target: 'table',
154
- tags: [{ namespace: 'codegen', tag: 'rename', args: ['Account'] }],
155
- },
156
- ],
157
- },
158
- ],
159
- })
160
- const result = generate(ctx)
161
- const content = result.files[0].content
162
- expect(content).toContain('class Account:')
163
- expect(content).not.toContain('class Users:')
164
- })
165
-
166
- it('respects @codegen.type override on column', () => {
167
- const ctx = makeCtx({
168
- allFileTags: [
169
- {
170
- sourceFile: 'test.sql',
171
- objects: [
172
- {
173
- objectName: 'users.metadata',
174
- target: 'column',
175
- tags: [{ namespace: 'codegen', tag: 'type', args: ['dict[str, Any]'] }],
176
- },
177
- ],
178
- },
179
- ],
180
- })
181
- const result = generate(ctx)
182
- const content = result.files[0].content
183
- expect(content).toContain('metadata: dict[str, Any]')
184
- })
185
- })