@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,148 @@
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
+ /**
7
+ * sqlc-style nullable type mapping.
8
+ * Nullable primitives use database/sql Null types instead of pointers.
9
+ */
10
+ const SQLC_NULL_MAP: Record<string, [string, string]> = {
11
+ string: ['sql.NullString', 'database/sql'],
12
+ int16: ['sql.NullInt16', 'database/sql'],
13
+ int32: ['sql.NullInt32', 'database/sql'],
14
+ int64: ['sql.NullInt64', 'database/sql'],
15
+ float32: ['sql.NullFloat64', 'database/sql'],
16
+ float64: ['sql.NullFloat64', 'database/sql'],
17
+ bool: ['sql.NullBool', 'database/sql'],
18
+ 'time.Time': ['sql.NullTime', 'database/sql'],
19
+ }
20
+
21
+ export default defineTemplate({
22
+ name: 'sqlc Models',
23
+ description: 'Generate Go structs matching sqlc naming conventions with sql.Null types',
24
+ language: 'go',
25
+
26
+ generate(ctx) {
27
+ const schema = enrichRealm(ctx)
28
+ const allImports = new Set<string>()
29
+ const structBlocks: string[] = []
30
+
31
+ // Enums
32
+ for (const e of schema.enums) {
33
+ const typeName = toPascalCase(e.name)
34
+ structBlocks.push(`type ${typeName} string`)
35
+ structBlocks.push('')
36
+ const constLines = e.values.map((v) => {
37
+ const constName = `${typeName}${toPascalCase(v)}`
38
+ return `\t${constName} ${typeName} = "${v}"`
39
+ })
40
+ structBlocks.push(`const (\n${constLines.join('\n')}\n)`)
41
+ }
42
+
43
+ // Composite types (collected from columns)
44
+ const composites = new Map<string, Array<{ name: string; type: string }>>()
45
+ for (const table of schema.tables) {
46
+ for (const col of table.columns) {
47
+ if (col.category === 'composite' && col.compositeFields?.length && !composites.has(col.pgType)) {
48
+ composites.set(col.pgType, col.compositeFields)
49
+ }
50
+ }
51
+ }
52
+ for (const view of schema.views) {
53
+ for (const col of view.columns) {
54
+ if (col.category === 'composite' && col.compositeFields?.length && !composites.has(col.pgType)) {
55
+ composites.set(col.pgType, col.compositeFields)
56
+ }
57
+ }
58
+ }
59
+ for (const [name, fields] of composites) {
60
+ const typeName = toPascalCase(name)
61
+ const fieldLines = fields.map((f) => {
62
+ const mapped = pgToGo(f.type, false)
63
+ for (const imp of mapped.imports) allImports.add(imp)
64
+ return `\t${toPascalCase(f.name)} ${mapped.type}`
65
+ })
66
+ structBlocks.push(`type ${typeName} struct {\n${fieldLines.join('\n')}\n}`)
67
+ }
68
+
69
+ // Helper to resolve sqlc-style Go type for a column
70
+ function resolveGoType(col: any): string {
71
+ if (col.typeOverride) return col.typeOverride
72
+ if (col.category === 'enum' && col.enumValues?.length) {
73
+ const enumType = toPascalCase(col.pgType)
74
+ if (col.nullable) {
75
+ // Use sql.NullString for nullable enums (sqlc convention)
76
+ allImports.add('database/sql')
77
+ return 'sql.NullString'
78
+ }
79
+ return enumType
80
+ }
81
+ if (col.category === 'composite' && col.compositeFields?.length) {
82
+ return col.nullable ? `*${toPascalCase(col.pgType)}` : toPascalCase(col.pgType)
83
+ }
84
+ // First get the non-nullable base type
85
+ const mapped = pgToGo(col.pgType, false, col.category)
86
+ const baseType = mapped.type
87
+ for (const imp of mapped.imports) allImports.add(imp)
88
+
89
+ if (col.nullable) {
90
+ // Check if this is an array type -- arrays stay as-is (nil slice is Go's nullable)
91
+ if (baseType.startsWith('[]')) {
92
+ return baseType
93
+ }
94
+ // Use sql.NullXxx for known types
95
+ const nullMapping = SQLC_NULL_MAP[baseType]
96
+ if (nullMapping) {
97
+ allImports.add(nullMapping[1])
98
+ return nullMapping[0]
99
+ }
100
+ // Fallback to pointer for complex types
101
+ return `*${baseType}`
102
+ }
103
+ return baseType
104
+ }
105
+
106
+ for (const table of activeTables(schema)) {
107
+ const fields: string[] = []
108
+
109
+ for (const col of table.columns) {
110
+ fields.push(`\t${col.pascalName} ${resolveGoType(col)}`)
111
+ }
112
+
113
+ structBlocks.push(`type ${table.pascalName} struct {\n${fields.join('\n')}\n}`)
114
+ }
115
+
116
+ // Views (read-only)
117
+ for (const view of schema.views.filter((v) => !v.skipped)) {
118
+ const fields: string[] = []
119
+
120
+ for (const col of view.columns) {
121
+ fields.push(`\t${col.pascalName} ${resolveGoType(col)}`)
122
+ }
123
+
124
+ structBlocks.push(
125
+ `// ${view.pascalName} is read-only (from view)\ntype ${view.pascalName} struct {\n${fields.join('\n')}\n}`,
126
+ )
127
+ }
128
+
129
+ let importBlock = ''
130
+ if (allImports.size > 0) {
131
+ const sorted = [...allImports].sort()
132
+ if (sorted.length === 1) {
133
+ importBlock = `import "${sorted[0]}"\n\n`
134
+ } else {
135
+ importBlock = `import (\n${sorted.map((i) => `\t"${i}"`).join('\n')}\n)\n\n`
136
+ }
137
+ }
138
+
139
+ const content = `// Generated by @sqldoc/templates/sqlc -- DO NOT EDIT
140
+ package models
141
+
142
+ ${importBlock}${structBlocks.join('\n\n')}\n`
143
+
144
+ return {
145
+ files: [{ path: 'models.go', content }],
146
+ }
147
+ },
148
+ })
@@ -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"]
@@ -0,0 +1,91 @@
1
+ package main
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "os"
7
+
8
+ "github.com/jackc/pgx/v5"
9
+
10
+ // Import the generated models to verify they compile
11
+ _ "sqldoc-test/models"
12
+ )
13
+
14
+ var failed int
15
+
16
+ func assert(condition bool, msg string) {
17
+ if !condition {
18
+ fmt.Fprintf(os.Stderr, "FAIL: %s\n", msg)
19
+ failed++
20
+ } else {
21
+ fmt.Printf(" ok: %s\n", msg)
22
+ }
23
+ }
24
+
25
+ func main() {
26
+ ctx := context.Background()
27
+ dbURL := os.Getenv("DATABASE_URL")
28
+ if dbURL == "" {
29
+ fmt.Fprintln(os.Stderr, "DATABASE_URL not set")
30
+ os.Exit(1)
31
+ }
32
+
33
+ conn, err := pgx.Connect(ctx, dbURL)
34
+ if err != nil {
35
+ fmt.Fprintf(os.Stderr, "connect error: %v\n", err)
36
+ os.Exit(1)
37
+ }
38
+ defer conn.Close(ctx)
39
+
40
+ fmt.Println("--- sqlc integration test ---")
41
+
42
+ // 1. Query known seeded user
43
+ var email, name string
44
+ var age int32
45
+ var isActive bool
46
+ err = conn.QueryRow(ctx, "SELECT email, name, age, is_active FROM users WHERE id = 1").
47
+ Scan(&email, &name, &age, &isActive)
48
+ if err != nil {
49
+ fmt.Fprintf(os.Stderr, "query user error: %v\n", err)
50
+ os.Exit(1)
51
+ }
52
+ assert(email == "test@example.com", "user email matches")
53
+ assert(name == "Test User", "user name matches")
54
+ assert(age == 30, "user age matches")
55
+ assert(isActive, "user is_active matches")
56
+
57
+ // 2. Query known seeded post
58
+ var title string
59
+ err = conn.QueryRow(ctx, "SELECT title FROM posts WHERE id = 1").Scan(&title)
60
+ if err != nil {
61
+ fmt.Fprintf(os.Stderr, "query post error: %v\n", err)
62
+ os.Exit(1)
63
+ }
64
+ assert(title == "Hello World", "post title matches")
65
+
66
+ // 3. Insert a new post
67
+ _, err = conn.Exec(ctx,
68
+ "INSERT INTO posts (user_id, title, body, view_count) VALUES (1, 'Post from sqlc', 'test body', 0)")
69
+ if err != nil {
70
+ fmt.Fprintf(os.Stderr, "insert error: %v\n", err)
71
+ os.Exit(1)
72
+ }
73
+
74
+ // 4. Read it back
75
+ var newTitle string
76
+ var userID int64
77
+ err = conn.QueryRow(ctx, "SELECT title, user_id FROM posts WHERE title = 'Post from sqlc'").
78
+ Scan(&newTitle, &userID)
79
+ if err != nil {
80
+ fmt.Fprintf(os.Stderr, "read back error: %v\n", err)
81
+ os.Exit(1)
82
+ }
83
+ assert(newTitle == "Post from sqlc", "inserted post title matches")
84
+ assert(userID == 1, "inserted post user_id matches")
85
+
86
+ if failed > 0 {
87
+ fmt.Fprintf(os.Stderr, "\n%d assertion(s) failed\n", failed)
88
+ os.Exit(1)
89
+ }
90
+ fmt.Println("\nAll assertions passed!")
91
+ }
@@ -0,0 +1,28 @@
1
+ /** Strip common leading whitespace from a tagged template literal */
2
+ export function dedent(strings: TemplateStringsArray, ...values: unknown[]): string {
3
+ // Interpolate
4
+ let result = ''
5
+ for (let i = 0; i < strings.length; i++) {
6
+ result += strings[i]
7
+ if (i < values.length) result += String(values[i])
8
+ }
9
+
10
+ // Split into lines
11
+ const lines = result.split('\n')
12
+
13
+ // Remove empty first/last lines
14
+ if (lines[0].trim() === '') lines.shift()
15
+ if (lines.length > 0 && lines[lines.length - 1].trim() === '') lines.pop()
16
+
17
+ // Find minimum indent (ignoring empty lines)
18
+ const indent = lines
19
+ .filter((l) => l.trim().length > 0)
20
+ .reduce((min, l) => {
21
+ const match = l.match(/^(\s+)/)
22
+ return match ? Math.min(min, match[1].length) : 0
23
+ }, Infinity)
24
+
25
+ if (indent === Infinity || indent === 0) return lines.join('\n')
26
+
27
+ return lines.map((l) => l.slice(indent)).join('\n')
28
+ }
@@ -0,0 +1,14 @@
1
+ import { dedent } from './dedent.ts'
2
+
3
+ // Re-export dedent for direct use
4
+ export { dedent }
5
+
6
+ // Each tag function is just dedent with a name (for grammar injection matching)
7
+ export const ts = dedent
8
+ export const go = dedent
9
+ export const python = dedent
10
+ export const sql = dedent
11
+ export const java = dedent
12
+ export const kotlin = dedent
13
+ export const rust = dedent
14
+ export const csharp = dedent
@@ -0,0 +1,8 @@
1
+ export { pgToCsharp } from './pg-to-csharp.ts'
2
+ export { pgToGo } from './pg-to-go.ts'
3
+ export { pgToJava } from './pg-to-java.ts'
4
+ export { pgToKotlin } from './pg-to-kotlin.ts'
5
+ export { pgToPython } from './pg-to-python.ts'
6
+ export { pgToRust } from './pg-to-rust.ts'
7
+ export type { TsTypeOptions } from './pg-to-ts.ts'
8
+ export { pgToTs } from './pg-to-ts.ts'
@@ -0,0 +1,136 @@
1
+ // C# value types that support nullable with ? suffix
2
+ const _VALUE_TYPES = new Set([
3
+ 'short',
4
+ 'int',
5
+ 'long',
6
+ 'float',
7
+ 'double',
8
+ 'decimal',
9
+ 'bool',
10
+ 'DateTime',
11
+ 'DateTimeOffset',
12
+ 'DateOnly',
13
+ 'TimeOnly',
14
+ 'TimeSpan',
15
+ 'Guid',
16
+ ])
17
+
18
+ /** Category-based mapping — used when Atlas provides a type category */
19
+ const CATEGORY_TO_CSHARP: Record<string, string> = {
20
+ string: 'string',
21
+ integer: 'long',
22
+ float: 'double',
23
+ decimal: 'decimal',
24
+ boolean: 'bool',
25
+ time: 'DateTime',
26
+ binary: 'byte[]',
27
+ json: 'string',
28
+ uuid: 'Guid',
29
+ spatial: 'string',
30
+ enum: 'string', // overridden per-column with actual enum type
31
+ unknown: 'string',
32
+ }
33
+
34
+ const PG_TO_CSHARP: Record<string, string> = {
35
+ // Numeric
36
+ smallint: 'short',
37
+ int2: 'short',
38
+ integer: 'int',
39
+ int: 'int',
40
+ int4: 'int',
41
+ bigint: 'long',
42
+ int8: 'long',
43
+ serial: 'int',
44
+ serial4: 'int',
45
+ bigserial: 'long',
46
+ serial8: 'long',
47
+ smallserial: 'short',
48
+ serial2: 'short',
49
+ real: 'float',
50
+ float4: 'float',
51
+ 'double precision': 'double',
52
+ float8: 'double',
53
+ numeric: 'decimal',
54
+ decimal: 'decimal',
55
+ money: 'decimal',
56
+
57
+ // String
58
+ text: 'string',
59
+ varchar: 'string',
60
+ 'character varying': 'string',
61
+ char: 'string',
62
+ character: 'string',
63
+ name: 'string',
64
+ citext: 'string',
65
+
66
+ // Boolean
67
+ boolean: 'bool',
68
+ bool: 'bool',
69
+
70
+ // Date/Time
71
+ timestamp: 'DateTime',
72
+ 'timestamp without time zone': 'DateTime',
73
+ timestamptz: 'DateTimeOffset',
74
+ 'timestamp with time zone': 'DateTimeOffset',
75
+ date: 'DateOnly',
76
+ time: 'TimeOnly',
77
+ 'time without time zone': 'TimeOnly',
78
+ timetz: 'TimeOnly',
79
+ 'time with time zone': 'TimeOnly',
80
+ interval: 'TimeSpan',
81
+
82
+ // Binary
83
+ bytea: 'byte[]',
84
+
85
+ // JSON
86
+ json: 'string',
87
+ jsonb: 'string',
88
+
89
+ // UUID
90
+ uuid: 'Guid',
91
+
92
+ // Network
93
+ inet: 'IPAddress',
94
+ cidr: 'string',
95
+ macaddr: 'string',
96
+ }
97
+
98
+ /**
99
+ * Map a PostgreSQL type to a C# type string.
100
+ * Nullable value types use ? suffix (int?), reference types use ? suffix (string?).
101
+ */
102
+ export function pgToCsharp(pgType: string, nullable: boolean, category?: string): string {
103
+ const normalized = pgType.toLowerCase().trim()
104
+
105
+ // Handle arrays
106
+ if (normalized.endsWith('[]')) {
107
+ const base = pgToCsharp(normalized.slice(0, -2), false)
108
+ return wrapNullable(`${base}[]`, nullable)
109
+ }
110
+ if (normalized.startsWith('_')) {
111
+ const base = pgToCsharp(normalized.slice(1), false)
112
+ return wrapNullable(`${base}[]`, nullable)
113
+ }
114
+
115
+ // Strip length specifiers
116
+ const baseType = normalized.replace(/\(\d+(?:,\s*\d+)?\)/, '').trim()
117
+
118
+ // Try raw type lookup first for precise mapping
119
+ let csType = PG_TO_CSHARP[baseType]
120
+
121
+ // Fall back to category-based mapping for unknown raw types
122
+ if (csType === undefined && category) {
123
+ csType = CATEGORY_TO_CSHARP[category]
124
+ }
125
+
126
+ if (csType === undefined) {
127
+ csType = 'string'
128
+ }
129
+
130
+ return wrapNullable(csType, nullable)
131
+ }
132
+
133
+ function wrapNullable(type: string, nullable: boolean): string {
134
+ if (!nullable) return type
135
+ return `${type}?`
136
+ }
@@ -0,0 +1,120 @@
1
+ /** Category-based mapping — used when Atlas provides a type category */
2
+ const CATEGORY_TO_GO: Record<string, [string, string | null]> = {
3
+ string: ['string', null],
4
+ integer: ['int64', null],
5
+ float: ['float64', null],
6
+ decimal: ['string', null],
7
+ boolean: ['bool', null],
8
+ time: ['time.Time', 'time'],
9
+ binary: ['[]byte', null],
10
+ json: ['json.RawMessage', 'encoding/json'],
11
+ uuid: ['uuid.UUID', 'github.com/google/uuid'],
12
+ spatial: ['string', null],
13
+ enum: ['string', null], // overridden per-column with actual enum type
14
+ unknown: ['interface{}', null],
15
+ }
16
+
17
+ const PG_TO_GO: Record<string, [string, string | null]> = {
18
+ // [goType, importPath]
19
+ // Numeric
20
+ smallint: ['int16', null],
21
+ int2: ['int16', null],
22
+ integer: ['int32', null],
23
+ int: ['int32', null],
24
+ int4: ['int32', null],
25
+ bigint: ['int64', null],
26
+ int8: ['int64', null],
27
+ serial: ['int32', null],
28
+ serial4: ['int32', null],
29
+ bigserial: ['int64', null],
30
+ serial8: ['int64', null],
31
+ smallserial: ['int16', null],
32
+ serial2: ['int16', null],
33
+ real: ['float32', null],
34
+ float4: ['float32', null],
35
+ 'double precision': ['float64', null],
36
+ float8: ['float64', null],
37
+ numeric: ['string', null],
38
+ decimal: ['string', null],
39
+ money: ['string', null],
40
+
41
+ // String
42
+ text: ['string', null],
43
+ varchar: ['string', null],
44
+ 'character varying': ['string', null],
45
+ char: ['string', null],
46
+ character: ['string', null],
47
+ name: ['string', null],
48
+ citext: ['string', null],
49
+
50
+ // Boolean
51
+ boolean: ['bool', null],
52
+ bool: ['bool', null],
53
+
54
+ // Date/Time
55
+ timestamp: ['time.Time', 'time'],
56
+ 'timestamp without time zone': ['time.Time', 'time'],
57
+ timestamptz: ['time.Time', 'time'],
58
+ 'timestamp with time zone': ['time.Time', 'time'],
59
+ date: ['time.Time', 'time'],
60
+ time: ['string', null],
61
+ 'time without time zone': ['string', null],
62
+ timetz: ['string', null],
63
+ 'time with time zone': ['string', null],
64
+ interval: ['string', null],
65
+
66
+ // Binary
67
+ bytea: ['[]byte', null],
68
+
69
+ // JSON
70
+ json: ['json.RawMessage', 'encoding/json'],
71
+ jsonb: ['json.RawMessage', 'encoding/json'],
72
+
73
+ // UUID
74
+ uuid: ['uuid.UUID', 'github.com/google/uuid'],
75
+
76
+ // Network
77
+ inet: ['string', null],
78
+ cidr: ['string', null],
79
+ macaddr: ['string', null],
80
+ macaddr8: ['string', null],
81
+ }
82
+
83
+ /**
84
+ * Map a PostgreSQL type to a Go type and required imports.
85
+ * Nullable Go types use pointers: *string, *int64, etc.
86
+ */
87
+ export function pgToGo(pgType: string, nullable: boolean, category?: string): { type: string; imports: string[] } {
88
+ const normalized = pgType.toLowerCase().trim()
89
+
90
+ // Handle arrays: text[] or _text
91
+ if (normalized.endsWith('[]')) {
92
+ const base = pgToGo(normalized.slice(0, -2), false)
93
+ return { type: `[]${base.type}`, imports: base.imports }
94
+ }
95
+ if (normalized.startsWith('_')) {
96
+ const base = pgToGo(normalized.slice(1), false)
97
+ return { type: `[]${base.type}`, imports: base.imports }
98
+ }
99
+
100
+ // Strip length specifiers
101
+ const baseType = normalized.replace(/\(\d+(?:,\s*\d+)?\)/, '').trim()
102
+
103
+ // Try raw type lookup first for precise mapping
104
+ let mapping = PG_TO_GO[baseType]
105
+
106
+ // Fall back to category-based mapping for unknown raw types
107
+ if (!mapping && category) {
108
+ mapping = CATEGORY_TO_GO[category]
109
+ }
110
+
111
+ if (!mapping) return { type: 'interface{}', imports: [] }
112
+
113
+ const [goType, importPath] = mapping
114
+ const imports = importPath ? [importPath] : []
115
+
116
+ if (nullable) {
117
+ return { type: `*${goType}`, imports }
118
+ }
119
+ return { type: goType, imports }
120
+ }