@sqldoc/templates 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.
Files changed (108) hide show
  1. package/package.json +161 -0
  2. package/src/__tests__/dedent.test.ts +45 -0
  3. package/src/__tests__/docker-templates.test.ts +134 -0
  4. package/src/__tests__/go-structs.test.ts +184 -0
  5. package/src/__tests__/naming.test.ts +48 -0
  6. package/src/__tests__/python-dataclasses.test.ts +185 -0
  7. package/src/__tests__/rust-structs.test.ts +176 -0
  8. package/src/__tests__/tags-helpers.test.ts +72 -0
  9. package/src/__tests__/type-mapping.test.ts +332 -0
  10. package/src/__tests__/typescript.test.ts +202 -0
  11. package/src/cobol-copybook/index.ts +220 -0
  12. package/src/cobol-copybook/test/.gitignore +6 -0
  13. package/src/cobol-copybook/test/Dockerfile +7 -0
  14. package/src/csharp-records/index.ts +131 -0
  15. package/src/csharp-records/test/.gitignore +6 -0
  16. package/src/csharp-records/test/Dockerfile +6 -0
  17. package/src/diesel/index.ts +247 -0
  18. package/src/diesel/test/.gitignore +6 -0
  19. package/src/diesel/test/Dockerfile +16 -0
  20. package/src/drizzle/index.ts +255 -0
  21. package/src/drizzle/test/.gitignore +6 -0
  22. package/src/drizzle/test/Dockerfile +8 -0
  23. package/src/drizzle/test/test.ts +71 -0
  24. package/src/efcore/index.ts +190 -0
  25. package/src/efcore/test/.gitignore +6 -0
  26. package/src/efcore/test/Dockerfile +7 -0
  27. package/src/go-structs/index.ts +119 -0
  28. package/src/go-structs/test/.gitignore +6 -0
  29. package/src/go-structs/test/Dockerfile +13 -0
  30. package/src/go-structs/test/test.go +71 -0
  31. package/src/gorm/index.ts +134 -0
  32. package/src/gorm/test/.gitignore +6 -0
  33. package/src/gorm/test/Dockerfile +13 -0
  34. package/src/gorm/test/test.go +65 -0
  35. package/src/helpers/atlas.ts +43 -0
  36. package/src/helpers/enrich.ts +396 -0
  37. package/src/helpers/naming.ts +19 -0
  38. package/src/helpers/tags.ts +63 -0
  39. package/src/index.ts +24 -0
  40. package/src/java-records/index.ts +179 -0
  41. package/src/java-records/test/.gitignore +6 -0
  42. package/src/java-records/test/Dockerfile +11 -0
  43. package/src/java-records/test/Test.java +93 -0
  44. package/src/jpa/index.ts +279 -0
  45. package/src/jpa/test/.gitignore +6 -0
  46. package/src/jpa/test/Dockerfile +14 -0
  47. package/src/jpa/test/Test.java +111 -0
  48. package/src/json-schema/index.ts +351 -0
  49. package/src/json-schema/test/.gitignore +6 -0
  50. package/src/json-schema/test/Dockerfile +18 -0
  51. package/src/knex/index.ts +168 -0
  52. package/src/knex/test/.gitignore +6 -0
  53. package/src/knex/test/Dockerfile +7 -0
  54. package/src/knex/test/test.ts +75 -0
  55. package/src/kotlin-data/index.ts +147 -0
  56. package/src/kotlin-data/test/.gitignore +6 -0
  57. package/src/kotlin-data/test/Dockerfile +14 -0
  58. package/src/kotlin-data/test/Test.kt +82 -0
  59. package/src/kysely/index.ts +165 -0
  60. package/src/kysely/test/.gitignore +6 -0
  61. package/src/kysely/test/Dockerfile +8 -0
  62. package/src/kysely/test/test.ts +82 -0
  63. package/src/prisma/index.ts +387 -0
  64. package/src/prisma/test/.gitignore +6 -0
  65. package/src/prisma/test/Dockerfile +7 -0
  66. package/src/protobuf/index.ts +219 -0
  67. package/src/protobuf/test/.gitignore +6 -0
  68. package/src/protobuf/test/Dockerfile +6 -0
  69. package/src/pydantic/index.ts +272 -0
  70. package/src/pydantic/test/.gitignore +6 -0
  71. package/src/pydantic/test/Dockerfile +8 -0
  72. package/src/pydantic/test/test.py +63 -0
  73. package/src/python-dataclasses/index.ts +217 -0
  74. package/src/python-dataclasses/test/.gitignore +6 -0
  75. package/src/python-dataclasses/test/Dockerfile +8 -0
  76. package/src/python-dataclasses/test/test.py +63 -0
  77. package/src/rust-structs/index.ts +152 -0
  78. package/src/rust-structs/test/.gitignore +6 -0
  79. package/src/rust-structs/test/Dockerfile +22 -0
  80. package/src/rust-structs/test/test.rs +82 -0
  81. package/src/sqlalchemy/index.ts +258 -0
  82. package/src/sqlalchemy/test/.gitignore +6 -0
  83. package/src/sqlalchemy/test/Dockerfile +8 -0
  84. package/src/sqlalchemy/test/test.py +61 -0
  85. package/src/sqlc/index.ts +148 -0
  86. package/src/sqlc/test/.gitignore +6 -0
  87. package/src/sqlc/test/Dockerfile +13 -0
  88. package/src/sqlc/test/test.go +91 -0
  89. package/src/tags/dedent.ts +28 -0
  90. package/src/tags/index.ts +14 -0
  91. package/src/types/index.ts +8 -0
  92. package/src/types/pg-to-csharp.ts +136 -0
  93. package/src/types/pg-to-go.ts +120 -0
  94. package/src/types/pg-to-java.ts +141 -0
  95. package/src/types/pg-to-kotlin.ts +119 -0
  96. package/src/types/pg-to-python.ts +120 -0
  97. package/src/types/pg-to-rust.ts +121 -0
  98. package/src/types/pg-to-ts.ts +173 -0
  99. package/src/typescript/index.ts +168 -0
  100. package/src/typescript/test/.gitignore +6 -0
  101. package/src/typescript/test/Dockerfile +8 -0
  102. package/src/typescript/test/test.ts +89 -0
  103. package/src/xsd/index.ts +191 -0
  104. package/src/xsd/test/.gitignore +6 -0
  105. package/src/xsd/test/Dockerfile +6 -0
  106. package/src/zod/index.ts +289 -0
  107. package/src/zod/test/.gitignore +6 -0
  108. package/src/zod/test/Dockerfile +6 -0
@@ -0,0 +1,255 @@
1
+ import { defineTemplate } from '@sqldoc/ns-codegen'
2
+ import { activeTables, enrichRealm } from '../helpers/enrich.ts'
3
+ import { toCamelCase } from '../helpers/naming.ts'
4
+
5
+ export const configSchema = {
6
+ schemaName: {
7
+ type: 'string',
8
+ description: 'PostgreSQL schema name override',
9
+ },
10
+ } as const
11
+
12
+ /** Track which drizzle-orm/pg-core imports are used */
13
+ interface ImportTracker {
14
+ used: Set<string>
15
+ }
16
+
17
+ /** Map a PostgreSQL type to a Drizzle column builder call */
18
+ function pgToDrizzle(pgType: string, colName: string, tracker: ImportTracker): string {
19
+ const normalized = pgType.toLowerCase().trim()
20
+
21
+ // Strip length specifiers for lookup
22
+ const baseType = normalized.replace(/\(\d+(?:,\s*\d+)?\)/, '').trim()
23
+
24
+ const map: Record<string, [string, string]> = {
25
+ // [drizzle-function-name, import-name]
26
+ smallint: ['smallint', 'smallint'],
27
+ int2: ['smallint', 'smallint'],
28
+ integer: ['integer', 'integer'],
29
+ int: ['integer', 'integer'],
30
+ int4: ['integer', 'integer'],
31
+ bigint: ['bigint', 'bigint'],
32
+ int8: ['bigint', 'bigint'],
33
+ serial: ['serial', 'serial'],
34
+ serial4: ['serial', 'serial'],
35
+ bigserial: ['bigserial', 'bigserial'],
36
+ serial8: ['bigserial', 'bigserial'],
37
+ smallserial: ['smallserial', 'smallserial'],
38
+ serial2: ['smallserial', 'smallserial'],
39
+ real: ['real', 'real'],
40
+ float4: ['real', 'real'],
41
+ 'double precision': ['doublePrecision', 'doublePrecision'],
42
+ float8: ['doublePrecision', 'doublePrecision'],
43
+ numeric: ['numeric', 'numeric'],
44
+ decimal: ['numeric', 'numeric'],
45
+ money: ['text', 'text'],
46
+
47
+ text: ['text', 'text'],
48
+ varchar: ['varchar', 'varchar'],
49
+ 'character varying': ['varchar', 'varchar'],
50
+ char: ['char', 'char'],
51
+ character: ['char', 'char'],
52
+ name: ['text', 'text'],
53
+ citext: ['text', 'text'],
54
+
55
+ boolean: ['boolean', 'boolean'],
56
+ bool: ['boolean', 'boolean'],
57
+
58
+ timestamp: ['timestamp', 'timestamp'],
59
+ 'timestamp without time zone': ['timestamp', 'timestamp'],
60
+ timestamptz: ['timestamp', 'timestamp'],
61
+ 'timestamp with time zone': ['timestamp', 'timestamp'],
62
+ date: ['date', 'date'],
63
+ time: ['time', 'time'],
64
+ 'time without time zone': ['time', 'time'],
65
+ timetz: ['time', 'time'],
66
+ 'time with time zone': ['time', 'time'],
67
+ interval: ['interval', 'interval'],
68
+
69
+ bytea: ['text', 'text'],
70
+
71
+ json: ['json', 'json'],
72
+ jsonb: ['jsonb', 'jsonb'],
73
+
74
+ uuid: ['uuid', 'uuid'],
75
+
76
+ inet: ['inet', 'inet'],
77
+ cidr: ['text', 'text'],
78
+ macaddr: ['macaddr', 'macaddr'],
79
+ macaddr8: ['macaddr8', 'macaddr8'],
80
+
81
+ xml: ['text', 'text'],
82
+ tsvector: ['text', 'text'],
83
+ tsquery: ['text', 'text'],
84
+ oid: ['integer', 'integer'],
85
+ point: ['text', 'text'],
86
+ line: ['text', 'text'],
87
+ box: ['text', 'text'],
88
+ circle: ['text', 'text'],
89
+ polygon: ['text', 'text'],
90
+ path: ['text', 'text'],
91
+ }
92
+
93
+ const entry = map[baseType]
94
+ if (entry) {
95
+ tracker.used.add(entry[1])
96
+ // bigint and bigserial require a mode option in drizzle-orm 0.38+
97
+ if (entry[0] === 'bigint' || entry[0] === 'bigserial') {
98
+ return `${entry[0]}('${colName}', { mode: 'number' })`
99
+ }
100
+ return `${entry[0]}('${colName}')`
101
+ }
102
+
103
+ // Fallback: arrays and unknowns
104
+ if (normalized.endsWith('[]') || normalized.startsWith('_')) {
105
+ tracker.used.add('text')
106
+ return `text('${colName}')`
107
+ }
108
+
109
+ tracker.used.add('text')
110
+ return `text('${colName}')`
111
+ }
112
+
113
+ /** Format a default expression for Drizzle */
114
+ function formatDefault(defaultValue: string | undefined): string | undefined {
115
+ if (!defaultValue) return undefined
116
+ // Heuristic: expressions with parens or uppercase keywords are likely SQL expressions
117
+ if (defaultValue.includes('(') || /^[A-Z_]+$/.test(defaultValue)) {
118
+ return `sql\`${defaultValue}\``
119
+ }
120
+ if (defaultValue === 'true' || defaultValue === 'false') return defaultValue
121
+ if (/^\d+$/.test(defaultValue)) return defaultValue
122
+ return `'${defaultValue}'`
123
+ }
124
+
125
+ export default defineTemplate({
126
+ name: 'Drizzle Schema',
127
+ description: 'Generate Drizzle ORM pgTable schema definitions from SQL schema',
128
+ language: 'typescript',
129
+ configSchema,
130
+
131
+ generate(ctx) {
132
+ const schema = enrichRealm(ctx)
133
+ const tables = activeTables(schema)
134
+ const tracker: ImportTracker = { used: new Set(['pgTable']) }
135
+ const tableBlocks: string[] = []
136
+
137
+ // We'll need a map from table name to camelCase variable name for FK references
138
+ const tableVarMap = new Map<string, string>()
139
+ for (const table of tables) {
140
+ tableVarMap.set(table.name, toCamelCase(table.pascalName))
141
+ }
142
+
143
+ // Enums
144
+ const enumBlocks: string[] = []
145
+ if (schema.enums.length > 0) {
146
+ tracker.used.add('pgEnum')
147
+ for (const e of schema.enums) {
148
+ const values = e.values.map((v) => `'${v}'`).join(', ')
149
+ enumBlocks.push(`export const ${toCamelCase(e.name)}Enum = pgEnum('${e.name}', [${values}])`)
150
+ }
151
+ }
152
+
153
+ for (const table of tables) {
154
+ const varName = toCamelCase(table.pascalName)
155
+
156
+ const colLines: string[] = []
157
+
158
+ for (const col of table.columns) {
159
+ let builder: string
160
+ if (col.typeOverride) {
161
+ tracker.used.add('text')
162
+ builder = `text('${col.name}')`
163
+ } else if (col.category === 'enum' && col.enumValues?.length) {
164
+ builder = `${toCamelCase(col.pgType)}Enum('${col.name}')`
165
+ } else {
166
+ builder = pgToDrizzle(col.pgType, col.name, tracker)
167
+ }
168
+
169
+ // Add modifiers
170
+ if (col.isPrimaryKey) {
171
+ builder += '.primaryKey()'
172
+ }
173
+ if (!col.nullable && !col.isSerial) {
174
+ builder += '.notNull()'
175
+ }
176
+
177
+ const defaultExpr = formatDefault(col.defaultValue)
178
+ if (defaultExpr) {
179
+ if (defaultExpr.startsWith('sql`')) {
180
+ tracker.used.add('sql')
181
+ builder += `.default(${defaultExpr})`
182
+ } else {
183
+ builder += `.default(${defaultExpr})`
184
+ }
185
+ }
186
+
187
+ // Foreign key references (skip self-references to avoid circular implicit-any in TS)
188
+ if (col.foreignKey && col.foreignKey.table !== table.name) {
189
+ const refVar = tableVarMap.get(col.foreignKey.table) ?? toCamelCase(col.foreignKey.table)
190
+ const refCol = col.foreignKey.column
191
+ builder += `.references(() => ${refVar}.${toCamelCase(refCol)})`
192
+ }
193
+
194
+ colLines.push(` ${toCamelCase(col.name)}: ${builder},`)
195
+ }
196
+
197
+ tableBlocks.push(`export const ${varName} = pgTable('${table.name}', {\n${colLines.join('\n')}\n})`)
198
+ }
199
+
200
+ // Views (read-only)
201
+ for (const view of schema.views.filter((v) => !v.skipped)) {
202
+ const varName = toCamelCase(view.pascalName)
203
+ tracker.used.add('pgView')
204
+
205
+ const colLines: string[] = []
206
+ for (const col of view.columns) {
207
+ let builder: string
208
+ if (col.typeOverride) {
209
+ tracker.used.add('text')
210
+ builder = `text('${col.name}')`
211
+ } else if (col.category === 'enum' && col.enumValues?.length) {
212
+ builder = `${toCamelCase(col.pgType)}Enum('${col.name}')`
213
+ } else {
214
+ builder = pgToDrizzle(col.pgType, col.name, tracker)
215
+ }
216
+
217
+ colLines.push(` ${toCamelCase(col.name)}: ${builder},`)
218
+ }
219
+
220
+ tableBlocks.push(
221
+ `/** Read-only (from view) */\nexport const ${varName} = pgView('${view.name}', {\n${colLines.join('\n')}\n})`,
222
+ )
223
+ }
224
+
225
+ // Build imports
226
+ const importNames = Array.from(tracker.used).sort()
227
+ const sqlImport = importNames.includes('sql')
228
+ const pgCoreImports = importNames.filter((n) => n !== 'sql')
229
+
230
+ const lines: string[] = [
231
+ '// Generated by @sqldoc/templates/drizzle -- DO NOT EDIT',
232
+ '',
233
+ `import { ${pgCoreImports.join(', ')} } from 'drizzle-orm/pg-core'`,
234
+ ]
235
+ if (sqlImport) {
236
+ lines.push("import { sql } from 'drizzle-orm'")
237
+ }
238
+ lines.push('')
239
+ if (enumBlocks.length > 0) {
240
+ lines.push(enumBlocks.join('\n'))
241
+ lines.push('')
242
+ }
243
+ lines.push(tableBlocks.join('\n\n'))
244
+ lines.push('')
245
+
246
+ return {
247
+ files: [
248
+ {
249
+ path: 'schema.ts',
250
+ content: lines.join('\n'),
251
+ },
252
+ ],
253
+ }
254
+ },
255
+ })
@@ -0,0 +1,6 @@
1
+ # Generated by codegen — only Dockerfile and test scripts are tracked
2
+ *
3
+ !.gitignore
4
+ !Dockerfile
5
+ !test.*
6
+ !Test.*
@@ -0,0 +1,8 @@
1
+ FROM node:23-slim
2
+ WORKDIR /app
3
+ COPY . .
4
+ RUN npm init -y && npm pkg set type=module && npm install typescript@5 drizzle-orm@0.38 pg @types/pg @types/node --save-dev
5
+ # Step 1: typecheck the generated schema
6
+ RUN npx tsc --noEmit --strict --esModuleInterop --module nodenext --moduleResolution nodenext --allowImportingTsExtensions --skipLibCheck *.ts
7
+ # Step 2: run integration test against real DB
8
+ CMD ["node", "--experimental-strip-types", "test.ts"]
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Integration test for @sqldoc/templates/drizzle
3
+ * Connects to real Postgres via drizzle-orm, verifies generated schema works.
4
+ */
5
+ import { drizzle } from 'drizzle-orm/node-postgres'
6
+ import { eq } from 'drizzle-orm'
7
+ import pg from 'pg'
8
+ import * as schema from './schema.ts'
9
+
10
+ const DATABASE_URL = process.env.DATABASE_URL
11
+ if (!DATABASE_URL) {
12
+ console.error('DATABASE_URL not set')
13
+ process.exit(1)
14
+ }
15
+
16
+ const pool = new pg.Pool({ connectionString: DATABASE_URL })
17
+ const db = drizzle(pool, { schema })
18
+
19
+ let failed = 0
20
+ function assert(condition: boolean, msg: string) {
21
+ if (!condition) {
22
+ console.error(`FAIL: ${msg}`)
23
+ failed++
24
+ } else {
25
+ console.log(` ok: ${msg}`)
26
+ }
27
+ }
28
+
29
+ async function run() {
30
+ try {
31
+ console.log('--- drizzle integration test ---')
32
+
33
+ // 1. Query known seeded user
34
+ const users = await db.select().from(schema.users).where(eq(schema.users.id, 1))
35
+ assert(users.length === 1, 'seeded user found')
36
+ assert(users[0].email === 'test@example.com', 'user email matches')
37
+ assert(users[0].name === 'Test User', 'user name matches')
38
+
39
+ // 2. Query known seeded post
40
+ const posts = await db.select().from(schema.posts).where(eq(schema.posts.id, 1))
41
+ assert(posts.length === 1, 'seeded post found')
42
+ assert(posts[0].title === 'Hello World', 'post title matches')
43
+
44
+ // 3. Insert a new post
45
+ await db.insert(schema.posts).values({
46
+ userId: 1,
47
+ title: 'Post from drizzle',
48
+ body: 'test body',
49
+ viewCount: 0,
50
+ })
51
+
52
+ // 4. Read it back
53
+ const newPosts = await db.select().from(schema.posts).where(eq(schema.posts.title, 'Post from drizzle'))
54
+ assert(newPosts.length === 1, 'inserted post found')
55
+ assert(newPosts[0].title === 'Post from drizzle', 'inserted post title matches')
56
+ assert(Number(newPosts[0].userId) === 1, 'inserted post user_id matches')
57
+
58
+ if (failed > 0) {
59
+ console.error(`\n${failed} assertion(s) failed`)
60
+ process.exit(1)
61
+ }
62
+ console.log('\nAll assertions passed!')
63
+ } finally {
64
+ await pool.end()
65
+ }
66
+ }
67
+
68
+ run().catch((err) => {
69
+ console.error(err)
70
+ process.exit(1)
71
+ })
@@ -0,0 +1,190 @@
1
+ import { defineTemplate } from '@sqldoc/ns-codegen'
2
+ import { activeTables, enrichRealm, type TagEntry } from '../helpers/enrich.ts'
3
+ import { toPascalCase } from '../helpers/naming.ts'
4
+ import { pgToCsharp } from '../types/pg-to-csharp.ts'
5
+
6
+ // C# reference types that get [Required] when non-nullable
7
+ const REFERENCE_TYPES = new Set(['string', 'byte[]', 'IPAddress'])
8
+
9
+ /**
10
+ * Extract varchar length from pgType, e.g. varchar(255) -> 255
11
+ */
12
+ function getVarcharLength(pgType: string): number | undefined {
13
+ const match = pgType.match(/(?:varchar|character varying)\((\d+)\)/i)
14
+ return match ? parseInt(match[1], 10) : undefined
15
+ }
16
+
17
+ /**
18
+ * Generate EF Core data annotations from @validate tags.
19
+ */
20
+ function getValidationAnnotations(colTags: TagEntry[]): string[] {
21
+ const annotations: string[] = []
22
+
23
+ for (const tag of colTags) {
24
+ if (tag.namespace !== 'validate') continue
25
+
26
+ if (tag.tag === 'length') {
27
+ const args = tag.args as Record<string, unknown>
28
+ if (args.max !== undefined) {
29
+ annotations.push(` [MaxLength(${args.max})]`)
30
+ }
31
+ } else if (tag.tag === 'range') {
32
+ const args = tag.args as Record<string, unknown>
33
+ const min = args.min ?? 0
34
+ const max = args.max ?? 0
35
+ annotations.push(` [Range(${min}, ${max})]`)
36
+ } else if (tag.tag === 'pattern') {
37
+ const pattern = Array.isArray(tag.args) ? tag.args[0] : undefined
38
+ if (pattern) {
39
+ annotations.push(` [RegularExpression(@"${pattern}")]`)
40
+ }
41
+ }
42
+ }
43
+
44
+ return annotations
45
+ }
46
+
47
+ export default defineTemplate({
48
+ name: 'EF Core Entities',
49
+ description: 'Generate Entity Framework Core entity classes with data annotations from SQL schema',
50
+ language: 'csharp',
51
+
52
+ generate(ctx) {
53
+ const schema = enrichRealm(ctx)
54
+ const classes: string[] = []
55
+
56
+ // Enums
57
+ for (const e of schema.enums) {
58
+ const enumName = toPascalCase(e.name)
59
+ const members = e.values.map((v) => ` ${toPascalCase(v)}`).join(',\n')
60
+ classes.push(`public enum ${enumName}\n{\n${members}\n}`)
61
+ }
62
+
63
+ // Composite types as [Owned] classes
64
+ const composites = new Map<string, Array<{ name: string; type: string }>>()
65
+ for (const table of schema.tables) {
66
+ for (const col of table.columns) {
67
+ if (col.category === 'composite' && col.compositeFields?.length && !composites.has(col.pgType)) {
68
+ composites.set(col.pgType, col.compositeFields)
69
+ }
70
+ }
71
+ }
72
+ for (const [name, fields] of composites) {
73
+ const className = toPascalCase(name)
74
+ const propertyLines: string[] = []
75
+ for (const f of fields) {
76
+ const csType = pgToCsharp(f.type, false)
77
+ propertyLines.push(` public ${csType} ${toPascalCase(f.name)} { get; set; }`)
78
+ propertyLines.push('')
79
+ }
80
+
81
+ classes.push(`[Owned]`)
82
+ classes.push(`public class ${className}`)
83
+ classes.push('{')
84
+ classes.push(propertyLines.join('\n'))
85
+ classes.push('}')
86
+ }
87
+
88
+ for (const table of activeTables(schema)) {
89
+ const propertyLines: string[] = []
90
+ for (const col of table.columns) {
91
+ let csType: string
92
+ if (col.typeOverride) {
93
+ csType = col.nullable ? `${col.typeOverride}?` : col.typeOverride
94
+ } else if (col.category === 'enum' && col.enumValues?.length) {
95
+ csType = col.nullable ? `${toPascalCase(col.pgType)}?` : toPascalCase(col.pgType)
96
+ } else if (col.category === 'composite' && col.compositeFields?.length) {
97
+ const compositeType = toPascalCase(col.pgType)
98
+ csType = col.nullable ? `${compositeType}?` : compositeType
99
+ } else {
100
+ csType = pgToCsharp(col.pgType, col.nullable, col.category)
101
+ }
102
+
103
+ const annotations: string[] = []
104
+
105
+ // PK annotation
106
+ if (col.isPrimaryKey) {
107
+ annotations.push(' [Key]')
108
+ }
109
+
110
+ // FK annotation
111
+ if (col.foreignKey) {
112
+ annotations.push(` [ForeignKey("${col.pascalName}")]`)
113
+ }
114
+
115
+ // Required for non-nullable reference types
116
+ const baseType = csType.replace('?', '')
117
+ if (!col.nullable && REFERENCE_TYPES.has(baseType) && !col.isPrimaryKey) {
118
+ annotations.push(' [Required]')
119
+ }
120
+
121
+ // MaxLength for varchar
122
+ const varcharLen = getVarcharLength(col.pgType)
123
+ if (varcharLen) {
124
+ annotations.push(` [MaxLength(${varcharLen})]`)
125
+ }
126
+
127
+ // Validation annotations from @validate tags
128
+ const validationAnnotations = getValidationAnnotations(col.tags)
129
+ annotations.push(...validationAnnotations)
130
+
131
+ if (annotations.length > 0) {
132
+ propertyLines.push(annotations.join('\n'))
133
+ }
134
+ propertyLines.push(` public ${csType} ${col.pascalName} { get; set; }`)
135
+ propertyLines.push('')
136
+ }
137
+
138
+ classes.push(`public class ${table.pascalName}`)
139
+ classes.push('{')
140
+ classes.push(propertyLines.join('\n'))
141
+ classes.push('}')
142
+ }
143
+
144
+ // Views (read-only — [Keyless] entities)
145
+ for (const view of schema.views.filter((v) => !v.skipped)) {
146
+ const propertyLines: string[] = []
147
+ for (const col of view.columns) {
148
+ let csType: string
149
+ if (col.typeOverride) {
150
+ csType = col.nullable ? `${col.typeOverride}?` : col.typeOverride
151
+ } else if (col.category === 'enum' && col.enumValues?.length) {
152
+ csType = col.nullable ? `${toPascalCase(col.pgType)}?` : toPascalCase(col.pgType)
153
+ } else if (col.category === 'composite' && col.compositeFields?.length) {
154
+ const compositeType = toPascalCase(col.pgType)
155
+ csType = col.nullable ? `${compositeType}?` : compositeType
156
+ } else {
157
+ csType = pgToCsharp(col.pgType, col.nullable, col.category)
158
+ }
159
+
160
+ propertyLines.push(` public ${csType} ${col.pascalName} { get; }`)
161
+ propertyLines.push('')
162
+ }
163
+
164
+ classes.push(`/// <summary>Read-only (from view)</summary>`)
165
+ classes.push(`[Keyless]`)
166
+ classes.push(`public class ${view.pascalName}`)
167
+ classes.push('{')
168
+ classes.push(propertyLines.join('\n'))
169
+ classes.push('}')
170
+ }
171
+
172
+ if (classes.length === 0) {
173
+ return { files: [] }
174
+ }
175
+
176
+ const parts: string[] = []
177
+ parts.push('using System.ComponentModel.DataAnnotations;')
178
+ parts.push('using System.ComponentModel.DataAnnotations.Schema;')
179
+ parts.push('using Microsoft.EntityFrameworkCore;')
180
+ parts.push('')
181
+ parts.push('namespace Generated;')
182
+ parts.push('')
183
+ parts.push(classes.join('\n\n'))
184
+ parts.push('')
185
+
186
+ return {
187
+ files: [{ path: 'Models.cs', content: parts.join('\n') }],
188
+ }
189
+ },
190
+ })
@@ -0,0 +1,6 @@
1
+ # Generated by codegen — only Dockerfile and test scripts are tracked
2
+ *
3
+ !.gitignore
4
+ !Dockerfile
5
+ !test.*
6
+ !Test.*
@@ -0,0 +1,7 @@
1
+ FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine
2
+ WORKDIR /app
3
+ RUN dotnet new classlib -n TypeCheck --force && rm TypeCheck/Class1.cs
4
+ RUN dotnet add TypeCheck/ package Microsoft.EntityFrameworkCore --version 9.0.0
5
+ COPY *.cs TypeCheck/
6
+ RUN dotnet build TypeCheck/
7
+ CMD ["echo", "ok"]
@@ -0,0 +1,119 @@
1
+ import { defineTemplate } from '@sqldoc/ns-codegen'
2
+ import { activeTables, enrichRealm } from '../helpers/enrich.ts'
3
+ import { toPascalCase } from '../helpers/naming.ts'
4
+ import { pgToGo } from '../types/pg-to-go.ts'
5
+
6
+ export default defineTemplate({
7
+ name: 'Go Structs',
8
+ description: 'Generate plain Go structs with json struct tags from SQL schema',
9
+ language: 'go',
10
+
11
+ generate(ctx) {
12
+ const schema = enrichRealm(ctx)
13
+ const allImports = new Set<string>()
14
+ const structBlocks: string[] = []
15
+
16
+ // Enums
17
+ for (const e of schema.enums) {
18
+ const typeName = toPascalCase(e.name)
19
+ structBlocks.push(`type ${typeName} string`)
20
+ structBlocks.push('')
21
+ const constLines = e.values.map((v) => {
22
+ const constName = `${typeName}${toPascalCase(v)}`
23
+ return `\t${constName} ${typeName} = "${v}"`
24
+ })
25
+ structBlocks.push(`const (\n${constLines.join('\n')}\n)`)
26
+ }
27
+
28
+ // Composite types (collected from columns)
29
+ const composites = new Map<string, Array<{ name: string; type: string }>>()
30
+ for (const table of schema.tables) {
31
+ for (const col of table.columns) {
32
+ if (col.category === 'composite' && col.compositeFields?.length && !composites.has(col.pgType)) {
33
+ composites.set(col.pgType, col.compositeFields)
34
+ }
35
+ }
36
+ }
37
+ for (const view of schema.views) {
38
+ for (const col of view.columns) {
39
+ if (col.category === 'composite' && col.compositeFields?.length && !composites.has(col.pgType)) {
40
+ composites.set(col.pgType, col.compositeFields)
41
+ }
42
+ }
43
+ }
44
+ for (const [name, fields] of composites) {
45
+ const typeName = toPascalCase(name)
46
+ const fieldLines = fields.map((f) => {
47
+ const mapped = pgToGo(f.type, false)
48
+ for (const imp of mapped.imports) allImports.add(imp)
49
+ const jsonTag = `\`json:"${f.name}"\``
50
+ return `\t${toPascalCase(f.name)} ${mapped.type} ${jsonTag}`
51
+ })
52
+ structBlocks.push(`type ${typeName} struct {\n${fieldLines.join('\n')}\n}`)
53
+ }
54
+
55
+ // Helper to resolve Go type for a column
56
+ function resolveGoType(col: any): string {
57
+ if (col.typeOverride) return col.typeOverride
58
+ if (col.category === 'enum' && col.enumValues?.length) {
59
+ return col.nullable ? `*${toPascalCase(col.pgType)}` : toPascalCase(col.pgType)
60
+ }
61
+ if (col.category === 'composite' && col.compositeFields?.length) {
62
+ return col.nullable ? `*${toPascalCase(col.pgType)}` : toPascalCase(col.pgType)
63
+ }
64
+ const mapped = pgToGo(col.pgType, col.nullable, col.category)
65
+ for (const imp of mapped.imports) allImports.add(imp)
66
+ return mapped.type
67
+ }
68
+
69
+ for (const table of activeTables(schema)) {
70
+ const fields: string[] = []
71
+
72
+ for (const col of table.columns) {
73
+ const goType = resolveGoType(col)
74
+
75
+ const jsonTag = col.nullable ? `\`json:"${col.name},omitempty"\`` : `\`json:"${col.name}"\``
76
+
77
+ fields.push(`\t${col.pascalName} ${goType} ${jsonTag}`)
78
+ }
79
+
80
+ structBlocks.push(`type ${table.pascalName} struct {\n${fields.join('\n')}\n}`)
81
+ }
82
+
83
+ // Views (read-only)
84
+ for (const view of schema.views.filter((v) => !v.skipped)) {
85
+ const fields: string[] = []
86
+
87
+ for (const col of view.columns) {
88
+ const goType = resolveGoType(col)
89
+
90
+ const jsonTag = col.nullable ? `\`json:"${col.name},omitempty"\`` : `\`json:"${col.name}"\``
91
+
92
+ fields.push(`\t${col.pascalName} ${goType} ${jsonTag}`)
93
+ }
94
+
95
+ structBlocks.push(
96
+ `// ${view.pascalName} is read-only (from view)\ntype ${view.pascalName} struct {\n${fields.join('\n')}\n}`,
97
+ )
98
+ }
99
+
100
+ let importBlock = ''
101
+ if (allImports.size > 0) {
102
+ const sorted = [...allImports].sort()
103
+ if (sorted.length === 1) {
104
+ importBlock = `import "${sorted[0]}"\n\n`
105
+ } else {
106
+ importBlock = `import (\n${sorted.map((i) => `\t"${i}"`).join('\n')}\n)\n\n`
107
+ }
108
+ }
109
+
110
+ const content = `// Generated by @sqldoc/templates/go-structs -- DO NOT EDIT
111
+ package models
112
+
113
+ ${importBlock}${structBlocks.join('\n\n')}\n`
114
+
115
+ return {
116
+ files: [{ path: 'models.go', content }],
117
+ }
118
+ },
119
+ })
@@ -0,0 +1,6 @@
1
+ # Generated by codegen — only Dockerfile and test scripts are tracked
2
+ *
3
+ !.gitignore
4
+ !Dockerfile
5
+ !test.*
6
+ !Test.*
@@ -0,0 +1,13 @@
1
+ FROM golang:1.24-alpine
2
+ WORKDIR /app
3
+ # Set up Go module with models as a sub-package
4
+ RUN go mod init sqldoc-test
5
+ RUN mkdir -p models cmd/test
6
+ COPY models.go models/
7
+ COPY test.go cmd/test/main.go
8
+ ENV GOTOOLCHAIN=auto
9
+ RUN go get github.com/jackc/pgx/v5 && go mod tidy
10
+ # Step 1: typecheck/compile all packages
11
+ RUN go build -o /usr/local/bin/test-runner ./cmd/test
12
+ # Step 2: run integration test against real DB
13
+ CMD ["test-runner"]