@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,141 @@
1
+ /** Category-based mapping — used when Atlas provides a type category */
2
+ const CATEGORY_TO_JAVA: Record<string, [string, string | null]> = {
3
+ string: ['String', null],
4
+ integer: ['long', null],
5
+ float: ['double', null],
6
+ decimal: ['BigDecimal', 'java.math.BigDecimal'],
7
+ boolean: ['boolean', null],
8
+ time: ['LocalDateTime', 'java.time.LocalDateTime'],
9
+ binary: ['byte[]', null],
10
+ json: ['String', null],
11
+ uuid: ['UUID', 'java.util.UUID'],
12
+ spatial: ['String', null],
13
+ enum: ['String', null], // overridden per-column with actual enum type
14
+ unknown: ['Object', null],
15
+ }
16
+
17
+ // Java uses wrapper types for nullable primitives
18
+ const PG_TO_JAVA: Record<string, [string, string | null]> = {
19
+ // [javaType, importPath] -- primitive types have wrapper equivalents for nullable
20
+ // Numeric
21
+ smallint: ['short', null],
22
+ int2: ['short', null],
23
+ integer: ['int', null],
24
+ int: ['int', null],
25
+ int4: ['int', null],
26
+ bigint: ['long', null],
27
+ int8: ['long', null],
28
+ serial: ['int', null],
29
+ serial4: ['int', null],
30
+ bigserial: ['long', null],
31
+ serial8: ['long', null],
32
+ smallserial: ['short', null],
33
+ serial2: ['short', null],
34
+ real: ['float', null],
35
+ float4: ['float', null],
36
+ 'double precision': ['double', null],
37
+ float8: ['double', null],
38
+ numeric: ['BigDecimal', 'java.math.BigDecimal'],
39
+ decimal: ['BigDecimal', 'java.math.BigDecimal'],
40
+ money: ['BigDecimal', 'java.math.BigDecimal'],
41
+
42
+ // String
43
+ text: ['String', null],
44
+ varchar: ['String', null],
45
+ 'character varying': ['String', null],
46
+ char: ['String', null],
47
+ character: ['String', null],
48
+ name: ['String', null],
49
+ citext: ['String', null],
50
+
51
+ // Boolean
52
+ boolean: ['boolean', null],
53
+ bool: ['boolean', null],
54
+
55
+ // Date/Time
56
+ timestamp: ['LocalDateTime', 'java.time.LocalDateTime'],
57
+ 'timestamp without time zone': ['LocalDateTime', 'java.time.LocalDateTime'],
58
+ timestamptz: ['OffsetDateTime', 'java.time.OffsetDateTime'],
59
+ 'timestamp with time zone': ['OffsetDateTime', 'java.time.OffsetDateTime'],
60
+ date: ['LocalDate', 'java.time.LocalDate'],
61
+ time: ['LocalTime', 'java.time.LocalTime'],
62
+ 'time without time zone': ['LocalTime', 'java.time.LocalTime'],
63
+ timetz: ['LocalTime', 'java.time.LocalTime'],
64
+ 'time with time zone': ['LocalTime', 'java.time.LocalTime'],
65
+ interval: ['Duration', 'java.time.Duration'],
66
+
67
+ // Binary
68
+ bytea: ['byte[]', null],
69
+
70
+ // JSON
71
+ json: ['String', null],
72
+ jsonb: ['String', null],
73
+
74
+ // UUID
75
+ uuid: ['UUID', 'java.util.UUID'],
76
+
77
+ // Network
78
+ inet: ['String', null],
79
+ cidr: ['String', null],
80
+ macaddr: ['String', null],
81
+ }
82
+
83
+ // Mapping from primitive types to their wrapper types for nullable
84
+ const PRIMITIVE_TO_WRAPPER: Record<string, string> = {
85
+ short: 'Short',
86
+ int: 'Integer',
87
+ long: 'Long',
88
+ float: 'Float',
89
+ double: 'Double',
90
+ boolean: 'Boolean',
91
+ 'byte[]': 'byte[]',
92
+ }
93
+
94
+ /**
95
+ * Map a PostgreSQL type to a Java type and required imports.
96
+ * Nullable primitive types use their wrapper equivalents (int -> Integer, long -> Long).
97
+ */
98
+ export function pgToJava(pgType: string, nullable: boolean, category?: string): { type: string; imports: string[] } {
99
+ const normalized = pgType.toLowerCase().trim()
100
+
101
+ // Handle arrays
102
+ if (normalized.endsWith('[]')) {
103
+ const base = pgToJava(normalized.slice(0, -2), false)
104
+ const wrappedType = PRIMITIVE_TO_WRAPPER[base.type] ?? base.type
105
+ const imports = [...base.imports]
106
+ if (!imports.includes('java.util.List')) imports.push('java.util.List')
107
+ return { type: `List<${wrappedType}>`, imports }
108
+ }
109
+ if (normalized.startsWith('_')) {
110
+ const base = pgToJava(normalized.slice(1), false)
111
+ const wrappedType = PRIMITIVE_TO_WRAPPER[base.type] ?? base.type
112
+ const imports = [...base.imports]
113
+ if (!imports.includes('java.util.List')) imports.push('java.util.List')
114
+ return { type: `List<${wrappedType}>`, imports }
115
+ }
116
+
117
+ // Strip length specifiers
118
+ const baseType = normalized.replace(/\(\d+(?:,\s*\d+)?\)/, '').trim()
119
+
120
+ // Try raw type lookup first for precise mapping
121
+ let mapping = PG_TO_JAVA[baseType]
122
+
123
+ // Fall back to category-based mapping for unknown raw types
124
+ if (!mapping && category) {
125
+ mapping = CATEGORY_TO_JAVA[category]
126
+ }
127
+
128
+ if (!mapping) return { type: 'Object', imports: [] }
129
+
130
+ const [javaType, importPath] = mapping
131
+ const imports = importPath ? [importPath] : []
132
+
133
+ if (nullable) {
134
+ const wrapper = PRIMITIVE_TO_WRAPPER[javaType]
135
+ if (wrapper) {
136
+ return { type: wrapper, imports }
137
+ }
138
+ }
139
+
140
+ return { type: javaType, imports }
141
+ }
@@ -0,0 +1,119 @@
1
+ /** Category-based mapping — used when Atlas provides a type category */
2
+ const CATEGORY_TO_KOTLIN: Record<string, string> = {
3
+ string: 'String',
4
+ integer: 'Long',
5
+ float: 'Double',
6
+ decimal: 'BigDecimal',
7
+ boolean: 'Boolean',
8
+ time: 'LocalDateTime',
9
+ binary: 'ByteArray',
10
+ json: 'String',
11
+ uuid: 'UUID',
12
+ spatial: 'String',
13
+ enum: 'String', // overridden per-column with actual enum type
14
+ unknown: 'String',
15
+ }
16
+
17
+ const PG_TO_KOTLIN: Record<string, string> = {
18
+ // Numeric
19
+ smallint: 'Short',
20
+ int2: 'Short',
21
+ integer: 'Int',
22
+ int: 'Int',
23
+ int4: 'Int',
24
+ bigint: 'Long',
25
+ int8: 'Long',
26
+ serial: 'Int',
27
+ serial4: 'Int',
28
+ bigserial: 'Long',
29
+ serial8: 'Long',
30
+ smallserial: 'Short',
31
+ serial2: 'Short',
32
+ real: 'Float',
33
+ float4: 'Float',
34
+ 'double precision': 'Double',
35
+ float8: 'Double',
36
+ numeric: 'BigDecimal',
37
+ decimal: 'BigDecimal',
38
+ money: 'BigDecimal',
39
+
40
+ // String
41
+ text: 'String',
42
+ varchar: 'String',
43
+ 'character varying': 'String',
44
+ char: 'String',
45
+ character: 'String',
46
+ name: 'String',
47
+ citext: 'String',
48
+
49
+ // Boolean
50
+ boolean: 'Boolean',
51
+ bool: 'Boolean',
52
+
53
+ // Date/Time
54
+ timestamp: 'LocalDateTime',
55
+ 'timestamp without time zone': 'LocalDateTime',
56
+ timestamptz: 'OffsetDateTime',
57
+ 'timestamp with time zone': 'OffsetDateTime',
58
+ date: 'LocalDate',
59
+ time: 'LocalTime',
60
+ 'time without time zone': 'LocalTime',
61
+ timetz: 'LocalTime',
62
+ 'time with time zone': 'LocalTime',
63
+ interval: 'Duration',
64
+
65
+ // Binary
66
+ bytea: 'ByteArray',
67
+
68
+ // JSON
69
+ json: 'String',
70
+ jsonb: 'String',
71
+
72
+ // UUID
73
+ uuid: 'UUID',
74
+
75
+ // Network
76
+ inet: 'String',
77
+ cidr: 'String',
78
+ macaddr: 'String',
79
+ }
80
+
81
+ /**
82
+ * Map a PostgreSQL type to a Kotlin type string.
83
+ * Nullable types use Kotlin's ? suffix: String -> String?
84
+ */
85
+ export function pgToKotlin(pgType: string, nullable: boolean, category?: string): string {
86
+ const normalized = pgType.toLowerCase().trim()
87
+
88
+ // Handle arrays
89
+ if (normalized.endsWith('[]')) {
90
+ const base = pgToKotlin(normalized.slice(0, -2), false)
91
+ return wrapNullable(`List<${base}>`, nullable)
92
+ }
93
+ if (normalized.startsWith('_')) {
94
+ const base = pgToKotlin(normalized.slice(1), false)
95
+ return wrapNullable(`List<${base}>`, nullable)
96
+ }
97
+
98
+ // Strip length specifiers
99
+ const baseType = normalized.replace(/\(\d+(?:,\s*\d+)?\)/, '').trim()
100
+
101
+ // Try raw type lookup first for precise mapping
102
+ let ktType = PG_TO_KOTLIN[baseType]
103
+
104
+ // Fall back to category-based mapping for unknown raw types
105
+ if (ktType === undefined && category) {
106
+ ktType = CATEGORY_TO_KOTLIN[category]
107
+ }
108
+
109
+ if (ktType === undefined) {
110
+ ktType = 'String'
111
+ }
112
+
113
+ return wrapNullable(ktType, nullable)
114
+ }
115
+
116
+ function wrapNullable(type: string, nullable: boolean): string {
117
+ if (!nullable) return type
118
+ return `${type}?`
119
+ }
@@ -0,0 +1,120 @@
1
+ /** Category-based mapping — used when Atlas provides a type category */
2
+ const CATEGORY_TO_PYTHON: Record<string, string> = {
3
+ string: 'str',
4
+ integer: 'int',
5
+ float: 'float',
6
+ decimal: 'Decimal',
7
+ boolean: 'bool',
8
+ time: 'datetime',
9
+ binary: 'bytes',
10
+ json: 'dict',
11
+ uuid: 'UUID',
12
+ spatial: 'str',
13
+ enum: 'str', // overridden per-column with actual enum type
14
+ unknown: 'Any',
15
+ }
16
+
17
+ const PG_TO_PYTHON: Record<string, string> = {
18
+ // Numeric
19
+ smallint: 'int',
20
+ int2: 'int',
21
+ integer: 'int',
22
+ int: 'int',
23
+ int4: 'int',
24
+ bigint: 'int',
25
+ int8: 'int',
26
+ serial: 'int',
27
+ serial4: 'int',
28
+ bigserial: 'int',
29
+ serial8: 'int',
30
+ smallserial: 'int',
31
+ serial2: 'int',
32
+ real: 'float',
33
+ float4: 'float',
34
+ 'double precision': 'float',
35
+ float8: 'float',
36
+ numeric: 'Decimal',
37
+ decimal: 'Decimal',
38
+ money: 'Decimal',
39
+
40
+ // String
41
+ text: 'str',
42
+ varchar: 'str',
43
+ 'character varying': 'str',
44
+ char: 'str',
45
+ character: 'str',
46
+ name: 'str',
47
+ citext: 'str',
48
+
49
+ // Boolean
50
+ boolean: 'bool',
51
+ bool: 'bool',
52
+
53
+ // Date/Time
54
+ timestamp: 'datetime',
55
+ 'timestamp without time zone': 'datetime',
56
+ timestamptz: 'datetime',
57
+ 'timestamp with time zone': 'datetime',
58
+ date: 'date',
59
+ time: 'time',
60
+ 'time without time zone': 'time',
61
+ timetz: 'time',
62
+ 'time with time zone': 'time',
63
+ interval: 'timedelta',
64
+
65
+ // Binary
66
+ bytea: 'bytes',
67
+
68
+ // JSON
69
+ json: 'dict',
70
+ jsonb: 'dict',
71
+
72
+ // UUID
73
+ uuid: 'UUID',
74
+
75
+ // Network
76
+ inet: 'str',
77
+ cidr: 'str',
78
+ macaddr: 'str',
79
+ macaddr8: 'str',
80
+ }
81
+
82
+ /**
83
+ * Map a PostgreSQL type to a Python type annotation.
84
+ * Nullable types use Optional[T].
85
+ */
86
+ export function pgToPython(pgType: string, nullable: boolean, category?: string): string {
87
+ const normalized = pgType.toLowerCase().trim()
88
+
89
+ // Handle arrays
90
+ if (normalized.endsWith('[]')) {
91
+ const base = pgToPython(normalized.slice(0, -2), false)
92
+ return wrapNullable(`list[${base}]`, nullable)
93
+ }
94
+ if (normalized.startsWith('_')) {
95
+ const base = pgToPython(normalized.slice(1), false)
96
+ return wrapNullable(`list[${base}]`, nullable)
97
+ }
98
+
99
+ // Strip length specifiers
100
+ const baseType = normalized.replace(/\(\d+(?:,\s*\d+)?\)/, '').trim()
101
+
102
+ // Try raw type lookup first for precise mapping
103
+ let pyType = PG_TO_PYTHON[baseType]
104
+
105
+ // Fall back to category-based mapping for unknown raw types
106
+ if (pyType === undefined && category) {
107
+ pyType = CATEGORY_TO_PYTHON[category]
108
+ }
109
+
110
+ if (pyType === undefined) {
111
+ pyType = 'Any'
112
+ }
113
+
114
+ return wrapNullable(pyType, nullable)
115
+ }
116
+
117
+ function wrapNullable(type: string, nullable: boolean): string {
118
+ if (!nullable) return type
119
+ return `Optional[${type}]`
120
+ }
@@ -0,0 +1,121 @@
1
+ /** Category-based mapping — used when Atlas provides a type category */
2
+ const CATEGORY_TO_RUST: Record<string, [string, string | null]> = {
3
+ string: ['String', null],
4
+ integer: ['i64', null],
5
+ float: ['f64', null],
6
+ decimal: ['bigdecimal::BigDecimal', 'bigdecimal::BigDecimal'],
7
+ boolean: ['bool', null],
8
+ time: ['NaiveDateTime', 'chrono::NaiveDateTime'],
9
+ binary: ['Vec<u8>', null],
10
+ json: ['serde_json::Value', 'serde_json::Value'],
11
+ uuid: ['Uuid', 'uuid::Uuid'],
12
+ spatial: ['String', null],
13
+ enum: ['String', null], // overridden per-column with actual enum type
14
+ unknown: ['String', null],
15
+ }
16
+
17
+ const PG_TO_RUST: Record<string, [string, string | null]> = {
18
+ // [rustType, importPath]
19
+ // Numeric
20
+ smallint: ['i16', null],
21
+ int2: ['i16', null],
22
+ integer: ['i32', null],
23
+ int: ['i32', null],
24
+ int4: ['i32', null],
25
+ bigint: ['i64', null],
26
+ int8: ['i64', null],
27
+ serial: ['i32', null],
28
+ serial4: ['i32', null],
29
+ bigserial: ['i64', null],
30
+ serial8: ['i64', null],
31
+ smallserial: ['i16', null],
32
+ serial2: ['i16', null],
33
+ real: ['f32', null],
34
+ float4: ['f32', null],
35
+ 'double precision': ['f64', null],
36
+ float8: ['f64', null],
37
+ numeric: ['bigdecimal::BigDecimal', 'bigdecimal::BigDecimal'],
38
+ decimal: ['bigdecimal::BigDecimal', 'bigdecimal::BigDecimal'],
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: ['NaiveDateTime', 'chrono::NaiveDateTime'],
56
+ 'timestamp without time zone': ['NaiveDateTime', 'chrono::NaiveDateTime'],
57
+ timestamptz: ['DateTime<Utc>', 'chrono::{DateTime, Utc}'],
58
+ 'timestamp with time zone': ['DateTime<Utc>', 'chrono::{DateTime, Utc}'],
59
+ date: ['NaiveDate', 'chrono::NaiveDate'],
60
+ time: ['NaiveTime', 'chrono::NaiveTime'],
61
+ 'time without time zone': ['NaiveTime', 'chrono::NaiveTime'],
62
+ timetz: ['NaiveTime', 'chrono::NaiveTime'],
63
+ 'time with time zone': ['NaiveTime', 'chrono::NaiveTime'],
64
+ interval: ['PgInterval', null],
65
+
66
+ // Binary
67
+ bytea: ['Vec<u8>', null],
68
+
69
+ // JSON
70
+ json: ['serde_json::Value', 'serde_json::Value'],
71
+ jsonb: ['serde_json::Value', 'serde_json::Value'],
72
+
73
+ // UUID
74
+ uuid: ['Uuid', 'uuid::Uuid'],
75
+
76
+ // Network
77
+ inet: ['String', null],
78
+ cidr: ['String', null],
79
+ macaddr: ['String', null],
80
+ }
81
+
82
+ /**
83
+ * Map a PostgreSQL type to a Rust type and required imports.
84
+ * Nullable types use Option<T>.
85
+ */
86
+ export function pgToRust(pgType: string, nullable: boolean, category?: string): { type: string; imports: string[] } {
87
+ const normalized = pgType.toLowerCase().trim()
88
+
89
+ // Handle arrays
90
+ if (normalized.endsWith('[]')) {
91
+ const base = pgToRust(normalized.slice(0, -2), false)
92
+ const vecType = `Vec<${base.type}>`
93
+ return nullable ? { type: `Option<${vecType}>`, imports: base.imports } : { type: vecType, imports: base.imports }
94
+ }
95
+ if (normalized.startsWith('_')) {
96
+ const base = pgToRust(normalized.slice(1), false)
97
+ const vecType = `Vec<${base.type}>`
98
+ return nullable ? { type: `Option<${vecType}>`, imports: base.imports } : { type: vecType, imports: base.imports }
99
+ }
100
+
101
+ // Strip length specifiers
102
+ const baseType = normalized.replace(/\(\d+(?:,\s*\d+)?\)/, '').trim()
103
+
104
+ // Try raw type lookup first for precise mapping
105
+ let mapping = PG_TO_RUST[baseType]
106
+
107
+ // Fall back to category-based mapping for unknown raw types
108
+ if (!mapping && category) {
109
+ mapping = CATEGORY_TO_RUST[category]
110
+ }
111
+
112
+ if (!mapping) return { type: 'String', imports: [] }
113
+
114
+ const [rustType, importPath] = mapping
115
+ const imports = importPath ? [importPath] : []
116
+
117
+ if (nullable) {
118
+ return { type: `Option<${rustType}>`, imports }
119
+ }
120
+ return { type: rustType, imports }
121
+ }
@@ -0,0 +1,173 @@
1
+ export interface TsTypeOptions {
2
+ dateType?: 'Date' | 'temporal' | 'dayjs' | 'luxon' | 'string'
3
+ nullableStyle?: 'optional' | 'null-union'
4
+ bigintType?: 'number' | 'bigint' | 'string'
5
+ }
6
+
7
+ /** Category-based mapping — used when Atlas provides a type category */
8
+ const CATEGORY_TO_TS: Record<string, string> = {
9
+ string: 'string',
10
+ integer: 'number',
11
+ float: 'number',
12
+ decimal: 'string',
13
+ boolean: 'boolean',
14
+ time: 'Date',
15
+ binary: 'Buffer',
16
+ json: 'unknown',
17
+ uuid: 'string',
18
+ spatial: 'string',
19
+ enum: 'string', // overridden per-column with actual enum type
20
+ unknown: 'unknown',
21
+ }
22
+
23
+ const PG_TO_TS: Record<string, string> = {
24
+ // Numeric
25
+ smallint: 'number',
26
+ int2: 'number',
27
+ integer: 'number',
28
+ int: 'number',
29
+ int4: 'number',
30
+ bigint: 'number',
31
+ int8: 'number',
32
+ serial: 'number',
33
+ serial4: 'number',
34
+ bigserial: 'number',
35
+ serial8: 'number',
36
+ smallserial: 'number',
37
+ serial2: 'number',
38
+ real: 'number',
39
+ float4: 'number',
40
+ 'double precision': 'number',
41
+ float8: 'number',
42
+ numeric: 'string',
43
+ decimal: 'string',
44
+ money: 'string',
45
+
46
+ // String
47
+ text: 'string',
48
+ varchar: 'string',
49
+ 'character varying': 'string',
50
+ char: 'string',
51
+ character: 'string',
52
+ name: 'string',
53
+ citext: 'string',
54
+
55
+ // Boolean
56
+ boolean: 'boolean',
57
+ bool: 'boolean',
58
+
59
+ // Date/Time
60
+ timestamp: 'Date',
61
+ 'timestamp without time zone': 'Date',
62
+ timestamptz: 'Date',
63
+ 'timestamp with time zone': 'Date',
64
+ date: 'string',
65
+ time: 'string',
66
+ 'time without time zone': 'string',
67
+ timetz: 'string',
68
+ 'time with time zone': 'string',
69
+ interval: 'string',
70
+
71
+ // Binary
72
+ bytea: 'Buffer',
73
+
74
+ // JSON
75
+ json: 'unknown',
76
+ jsonb: 'unknown',
77
+
78
+ // UUID
79
+ uuid: 'string',
80
+
81
+ // Network
82
+ inet: 'string',
83
+ cidr: 'string',
84
+ macaddr: 'string',
85
+ macaddr8: 'string',
86
+
87
+ // Geometric
88
+ point: 'string',
89
+ line: 'string',
90
+ box: 'string',
91
+ circle: 'string',
92
+ polygon: 'string',
93
+ path: 'string',
94
+
95
+ // Other
96
+ xml: 'string',
97
+ tsvector: 'string',
98
+ tsquery: 'string',
99
+ oid: 'number',
100
+ }
101
+
102
+ /** Temporal API mappings — each SQL date/time type maps to the right Temporal type */
103
+ const TEMPORAL_MAP: Record<string, string> = {
104
+ date: 'Temporal.PlainDate',
105
+ timestamp: 'Temporal.PlainDateTime',
106
+ 'timestamp without time zone': 'Temporal.PlainDateTime',
107
+ timestamptz: 'Temporal.ZonedDateTime',
108
+ 'timestamp with time zone': 'Temporal.ZonedDateTime',
109
+ time: 'Temporal.PlainTime',
110
+ 'time without time zone': 'Temporal.PlainTime',
111
+ timetz: 'string',
112
+ 'time with time zone': 'string',
113
+ interval: 'Temporal.Duration',
114
+ }
115
+
116
+ /**
117
+ * Map a PostgreSQL type to a TypeScript type string.
118
+ * Handles arrays (text[], _text), length specifiers (varchar(255)), and nullability.
119
+ */
120
+ export function pgToTs(pgType: string, nullable: boolean, options: TsTypeOptions = {}, category?: string): string {
121
+ const normalized = pgType.toLowerCase().trim()
122
+
123
+ // Handle arrays: text[] or _text
124
+ if (normalized.endsWith('[]')) {
125
+ const base = pgToTs(normalized.slice(0, -2), false, options)
126
+ return wrapNullable(`${base}[]`, nullable, options.nullableStyle)
127
+ }
128
+ if (normalized.startsWith('_')) {
129
+ const base = pgToTs(normalized.slice(1), false, options)
130
+ return wrapNullable(`${base}[]`, nullable, options.nullableStyle)
131
+ }
132
+
133
+ // Strip length specifiers: varchar(255) -> varchar, numeric(10,2) -> numeric
134
+ const baseType = normalized.replace(/\(\d+(?:,\s*\d+)?\)/, '').trim()
135
+
136
+ // Try raw type lookup first for precise mapping
137
+ let tsType = PG_TO_TS[baseType]
138
+
139
+ // Fall back to category-based mapping for unknown raw types
140
+ if (tsType === undefined && category) {
141
+ tsType = CATEGORY_TO_TS[category]
142
+ }
143
+
144
+ if (tsType === undefined) {
145
+ tsType = 'unknown'
146
+ }
147
+
148
+ // Apply bigintType option
149
+ if (
150
+ (baseType === 'bigint' || baseType === 'int8' || baseType === 'bigserial' || baseType === 'serial8') &&
151
+ options.bigintType
152
+ ) {
153
+ tsType = options.bigintType
154
+ }
155
+
156
+ // Apply dateType option
157
+ if (options.dateType === 'temporal') {
158
+ const temporal = TEMPORAL_MAP[baseType]
159
+ if (temporal) tsType = temporal
160
+ } else if (options.dateType && options.dateType !== 'Date') {
161
+ if (tsType === 'Date') {
162
+ tsType = options.dateType === 'string' ? 'string' : `import('${options.dateType}').Dayjs`
163
+ }
164
+ }
165
+
166
+ return wrapNullable(tsType, nullable, options.nullableStyle)
167
+ }
168
+
169
+ function wrapNullable(type: string, nullable: boolean, style?: 'optional' | 'null-union'): string {
170
+ if (!nullable) return type
171
+ if (style === 'null-union') return `${type} | null`
172
+ return type // 'optional' style is handled at property level with ?:
173
+ }