@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.
- package/package.json +161 -0
- package/src/__tests__/dedent.test.ts +45 -0
- package/src/__tests__/docker-templates.test.ts +134 -0
- package/src/__tests__/go-structs.test.ts +184 -0
- package/src/__tests__/naming.test.ts +48 -0
- package/src/__tests__/python-dataclasses.test.ts +185 -0
- package/src/__tests__/rust-structs.test.ts +176 -0
- package/src/__tests__/tags-helpers.test.ts +72 -0
- package/src/__tests__/type-mapping.test.ts +332 -0
- package/src/__tests__/typescript.test.ts +202 -0
- package/src/cobol-copybook/index.ts +220 -0
- package/src/cobol-copybook/test/.gitignore +6 -0
- package/src/cobol-copybook/test/Dockerfile +7 -0
- package/src/csharp-records/index.ts +131 -0
- package/src/csharp-records/test/.gitignore +6 -0
- package/src/csharp-records/test/Dockerfile +6 -0
- package/src/diesel/index.ts +247 -0
- package/src/diesel/test/.gitignore +6 -0
- package/src/diesel/test/Dockerfile +16 -0
- package/src/drizzle/index.ts +255 -0
- package/src/drizzle/test/.gitignore +6 -0
- package/src/drizzle/test/Dockerfile +8 -0
- package/src/drizzle/test/test.ts +71 -0
- package/src/efcore/index.ts +190 -0
- package/src/efcore/test/.gitignore +6 -0
- package/src/efcore/test/Dockerfile +7 -0
- package/src/go-structs/index.ts +119 -0
- package/src/go-structs/test/.gitignore +6 -0
- package/src/go-structs/test/Dockerfile +13 -0
- package/src/go-structs/test/test.go +71 -0
- package/src/gorm/index.ts +134 -0
- package/src/gorm/test/.gitignore +6 -0
- package/src/gorm/test/Dockerfile +13 -0
- package/src/gorm/test/test.go +65 -0
- package/src/helpers/atlas.ts +43 -0
- package/src/helpers/enrich.ts +396 -0
- package/src/helpers/naming.ts +19 -0
- package/src/helpers/tags.ts +63 -0
- package/src/index.ts +24 -0
- package/src/java-records/index.ts +179 -0
- package/src/java-records/test/.gitignore +6 -0
- package/src/java-records/test/Dockerfile +11 -0
- package/src/java-records/test/Test.java +93 -0
- package/src/jpa/index.ts +279 -0
- package/src/jpa/test/.gitignore +6 -0
- package/src/jpa/test/Dockerfile +14 -0
- package/src/jpa/test/Test.java +111 -0
- package/src/json-schema/index.ts +351 -0
- package/src/json-schema/test/.gitignore +6 -0
- package/src/json-schema/test/Dockerfile +18 -0
- package/src/knex/index.ts +168 -0
- package/src/knex/test/.gitignore +6 -0
- package/src/knex/test/Dockerfile +7 -0
- package/src/knex/test/test.ts +75 -0
- package/src/kotlin-data/index.ts +147 -0
- package/src/kotlin-data/test/.gitignore +6 -0
- package/src/kotlin-data/test/Dockerfile +14 -0
- package/src/kotlin-data/test/Test.kt +82 -0
- package/src/kysely/index.ts +165 -0
- package/src/kysely/test/.gitignore +6 -0
- package/src/kysely/test/Dockerfile +8 -0
- package/src/kysely/test/test.ts +82 -0
- package/src/prisma/index.ts +387 -0
- package/src/prisma/test/.gitignore +6 -0
- package/src/prisma/test/Dockerfile +7 -0
- package/src/protobuf/index.ts +219 -0
- package/src/protobuf/test/.gitignore +6 -0
- package/src/protobuf/test/Dockerfile +6 -0
- package/src/pydantic/index.ts +272 -0
- package/src/pydantic/test/.gitignore +6 -0
- package/src/pydantic/test/Dockerfile +8 -0
- package/src/pydantic/test/test.py +63 -0
- package/src/python-dataclasses/index.ts +217 -0
- package/src/python-dataclasses/test/.gitignore +6 -0
- package/src/python-dataclasses/test/Dockerfile +8 -0
- package/src/python-dataclasses/test/test.py +63 -0
- package/src/rust-structs/index.ts +152 -0
- package/src/rust-structs/test/.gitignore +6 -0
- package/src/rust-structs/test/Dockerfile +22 -0
- package/src/rust-structs/test/test.rs +82 -0
- package/src/sqlalchemy/index.ts +258 -0
- package/src/sqlalchemy/test/.gitignore +6 -0
- package/src/sqlalchemy/test/Dockerfile +8 -0
- package/src/sqlalchemy/test/test.py +61 -0
- package/src/sqlc/index.ts +148 -0
- package/src/sqlc/test/.gitignore +6 -0
- package/src/sqlc/test/Dockerfile +13 -0
- package/src/sqlc/test/test.go +91 -0
- package/src/tags/dedent.ts +28 -0
- package/src/tags/index.ts +14 -0
- package/src/types/index.ts +8 -0
- package/src/types/pg-to-csharp.ts +136 -0
- package/src/types/pg-to-go.ts +120 -0
- package/src/types/pg-to-java.ts +141 -0
- package/src/types/pg-to-kotlin.ts +119 -0
- package/src/types/pg-to-python.ts +120 -0
- package/src/types/pg-to-rust.ts +121 -0
- package/src/types/pg-to-ts.ts +173 -0
- package/src/typescript/index.ts +168 -0
- package/src/typescript/test/.gitignore +6 -0
- package/src/typescript/test/Dockerfile +8 -0
- package/src/typescript/test/test.ts +89 -0
- package/src/xsd/index.ts +191 -0
- package/src/xsd/test/.gitignore +6 -0
- package/src/xsd/test/Dockerfile +6 -0
- package/src/zod/index.ts +289 -0
- package/src/zod/test/.gitignore +6 -0
- 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,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
|
+
}
|