@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,93 @@
1
+ import java.sql.*;
2
+ import java.time.OffsetDateTime;
3
+
4
+ /**
5
+ * Integration test for @sqldoc/templates/java-records
6
+ * Connects to real Postgres, verifies generated records work with actual data.
7
+ */
8
+ public class Test {
9
+ static int failed = 0;
10
+
11
+ static void assertEq(Object actual, Object expected, String msg) {
12
+ if (!actual.equals(expected)) {
13
+ System.err.printf("FAIL: %s (got %s, expected %s)%n", msg, actual, expected);
14
+ failed++;
15
+ } else {
16
+ System.out.printf(" ok: %s%n", msg);
17
+ }
18
+ }
19
+
20
+ public static void main(String[] args) throws Exception {
21
+ String dbUrl = System.getenv("DATABASE_URL");
22
+ if (dbUrl == null || dbUrl.isEmpty()) {
23
+ System.err.println("DATABASE_URL not set");
24
+ System.exit(1);
25
+ }
26
+
27
+ // Convert postgres(ql):// to jdbc:postgresql://, extracting userinfo for JDBC
28
+ var uri = java.net.URI.create(dbUrl.replaceFirst("^postgres(ql)?://", "http://"));
29
+ var userInfo = uri.getUserInfo();
30
+ var jdbcUrl = "jdbc:postgresql://" + uri.getHost() + ":" + (uri.getPort() > 0 ? uri.getPort() : 5432) + uri.getPath();
31
+ var query = uri.getQuery();
32
+ if (userInfo != null) {
33
+ var parts = userInfo.split(":", 2);
34
+ var sep = query != null ? "&" : "?";
35
+ jdbcUrl += (query != null ? "?" + query : "") + sep + "user=" + parts[0] + "&password=" + (parts.length > 1 ? parts[1] : "");
36
+ } else if (query != null) {
37
+ jdbcUrl += "?" + query;
38
+ }
39
+ dbUrl = jdbcUrl;
40
+
41
+ System.out.println("--- java-records integration test ---");
42
+
43
+ try (Connection conn = DriverManager.getConnection(dbUrl)) {
44
+ // 1. Query user and construct generated record
45
+ try (PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = 1")) {
46
+ ResultSet rs = ps.executeQuery();
47
+ rs.next();
48
+ var user = new Users(
49
+ rs.getLong("id"),
50
+ rs.getString("email"),
51
+ rs.getString("name"),
52
+ rs.getObject("age") != null ? rs.getInt("age") : null,
53
+ rs.getBoolean("is_active"),
54
+ rs.getString("metadata"),
55
+ null, // address (composite)
56
+ rs.getObject("created_at", OffsetDateTime.class),
57
+ null, // tags (array)
58
+ rs.getBytes("avatar"),
59
+ rs.getBigDecimal("balance"),
60
+ rs.getObject("external_id") != null ? java.util.UUID.fromString(rs.getString("external_id")) : null
61
+ );
62
+ assertEq(user.email(), "test@example.com", "user.email() matches");
63
+ assertEq(user.name(), "Test User", "user.name() matches");
64
+ assertEq(user.age(), 30, "user.age() matches");
65
+ assertEq(user.isActive(), true, "user.isActive() matches");
66
+ }
67
+
68
+ // 2. Query post and construct generated record
69
+ try (PreparedStatement ps = conn.prepareStatement("SELECT * FROM posts WHERE id = 1")) {
70
+ ResultSet rs = ps.executeQuery();
71
+ rs.next();
72
+ var post = new Posts(
73
+ rs.getLong("id"),
74
+ rs.getLong("user_id"),
75
+ rs.getString("title"),
76
+ rs.getString("body"),
77
+ rs.getObject("published_at") != null ? rs.getObject("published_at", OffsetDateTime.class) : null,
78
+ rs.getInt("view_count"),
79
+ rs.getObject("rating") != null ? rs.getDouble("rating") : null
80
+ );
81
+ assertEq(post.title(), "Hello World", "post.title() matches");
82
+ assertEq(post.userId(), 1L, "post.userId() matches");
83
+ assertEq(post.viewCount(), 42, "post.viewCount() matches");
84
+ }
85
+ }
86
+
87
+ if (failed > 0) {
88
+ System.err.printf("%n%d assertion(s) failed%n", failed);
89
+ System.exit(1);
90
+ }
91
+ System.out.println("\nAll assertions passed!");
92
+ }
93
+ }
@@ -0,0 +1,279 @@
1
+ import { defineTemplate } from '@sqldoc/ns-codegen'
2
+ import { activeTables, enrichRealm, type TagEntry } from '../helpers/enrich.ts'
3
+ import { toCamelCase, toPascalCase, toScreamingSnake } from '../helpers/naming.ts'
4
+ import { pgToJava } from '../types/pg-to-java.ts'
5
+
6
+ /**
7
+ * Extract varchar length from pgType, e.g. varchar(255) -> 255
8
+ */
9
+ function getVarcharLength(pgType: string): number | undefined {
10
+ const match = pgType.match(/(?:varchar|character varying)\((\d+)\)/i)
11
+ return match ? parseInt(match[1], 10) : undefined
12
+ }
13
+
14
+ /**
15
+ * Generate validation annotations from @validate tags.
16
+ */
17
+ function getValidationAnnotations(colTags: TagEntry[]): { annotations: string[]; imports: Set<string> } {
18
+ const annotations: string[] = []
19
+ const imports = new Set<string>()
20
+
21
+ for (const tag of colTags) {
22
+ if (tag.namespace !== 'validate') continue
23
+
24
+ if (tag.tag === 'notEmpty') {
25
+ annotations.push('@NotEmpty')
26
+ imports.add('jakarta.validation.constraints.NotEmpty')
27
+ } else if (tag.tag === 'length') {
28
+ const args = tag.args as Record<string, unknown>
29
+ const parts: string[] = []
30
+ if (args.min !== undefined) parts.push(`min = ${args.min}`)
31
+ if (args.max !== undefined) parts.push(`max = ${args.max}`)
32
+ annotations.push(`@Size(${parts.join(', ')})`)
33
+ imports.add('jakarta.validation.constraints.Size')
34
+ } else if (tag.tag === 'range') {
35
+ const args = tag.args as Record<string, unknown>
36
+ if (args.min !== undefined) {
37
+ annotations.push(`@Min(${args.min})`)
38
+ imports.add('jakarta.validation.constraints.Min')
39
+ }
40
+ if (args.max !== undefined) {
41
+ annotations.push(`@Max(${args.max})`)
42
+ imports.add('jakarta.validation.constraints.Max')
43
+ }
44
+ } else if (tag.tag === 'pattern') {
45
+ const pattern = Array.isArray(tag.args) ? tag.args[0] : undefined
46
+ if (pattern) {
47
+ // Escape backslashes for Java string literals
48
+ const escaped = pattern.replace(/\\/g, '\\\\')
49
+ annotations.push(`@Pattern(regexp = "${escaped}")`)
50
+ imports.add('jakarta.validation.constraints.Pattern')
51
+ }
52
+ }
53
+ }
54
+
55
+ return { annotations, imports }
56
+ }
57
+
58
+ export default defineTemplate({
59
+ name: 'JPA Entities',
60
+ description: 'Generate JPA @Entity classes with annotations from SQL schema',
61
+ language: 'java',
62
+
63
+ generate(ctx) {
64
+ const schema = enrichRealm(ctx)
65
+ const files: Array<{ path: string; content: string }> = []
66
+
67
+ // Enums
68
+ for (const e of schema.enums) {
69
+ const className = toPascalCase(e.name)
70
+ const members = e.values.map((v) => {
71
+ const constName = toScreamingSnake(v)
72
+ return ` ${constName}("${v}")`
73
+ })
74
+
75
+ const parts: string[] = []
76
+ parts.push(`public enum ${className} {`)
77
+ parts.push(`${members.join(',\n')};`)
78
+ parts.push('')
79
+ parts.push(` private final String value;`)
80
+ parts.push('')
81
+ parts.push(` ${className}(String value) {`)
82
+ parts.push(` this.value = value;`)
83
+ parts.push(` }`)
84
+ parts.push('')
85
+ parts.push(` public String getValue() {`)
86
+ parts.push(` return value;`)
87
+ parts.push(` }`)
88
+ parts.push('}')
89
+ parts.push('')
90
+
91
+ files.push({
92
+ path: `${className}.java`,
93
+ content: parts.join('\n'),
94
+ })
95
+ }
96
+
97
+ // Composite types as @Embeddable classes
98
+ const composites = new Map<string, Array<{ name: string; type: string }>>()
99
+ for (const table of schema.tables) {
100
+ for (const col of table.columns) {
101
+ if (col.category === 'composite' && col.compositeFields?.length && !composites.has(col.pgType)) {
102
+ composites.set(col.pgType, col.compositeFields)
103
+ }
104
+ }
105
+ }
106
+ for (const [name, fields] of composites) {
107
+ const className = toPascalCase(name)
108
+ const allImports = new Set<string>()
109
+ allImports.add('jakarta.persistence.Embeddable')
110
+
111
+ const fieldLines: string[] = []
112
+ for (const f of fields) {
113
+ const mapped = pgToJava(f.type, false)
114
+ for (const imp of mapped.imports) allImports.add(imp)
115
+ fieldLines.push(` private ${mapped.type} ${toCamelCase(f.name)};`)
116
+ fieldLines.push('')
117
+ }
118
+
119
+ const sortedImports = [...allImports].sort()
120
+ const importLines = sortedImports.map((imp) => `import ${imp};`)
121
+
122
+ const parts: string[] = []
123
+ parts.push(importLines.join('\n'))
124
+ parts.push('')
125
+ parts.push('@Embeddable')
126
+ parts.push(`public class ${className} {`)
127
+ parts.push('')
128
+ parts.push(fieldLines.join('\n'))
129
+ parts.push('}')
130
+ parts.push('')
131
+
132
+ files.push({
133
+ path: `${className}.java`,
134
+ content: parts.join('\n'),
135
+ })
136
+ }
137
+
138
+ for (const table of activeTables(schema)) {
139
+ const allImports = new Set<string>()
140
+ allImports.add('jakarta.persistence.*')
141
+
142
+ const fieldLines: string[] = []
143
+ for (const col of table.columns) {
144
+ let javaType: string
145
+ if (col.typeOverride) {
146
+ javaType = col.typeOverride
147
+ } else if (col.category === 'enum' && col.enumValues?.length) {
148
+ javaType = toPascalCase(col.pgType)
149
+ } else if (col.category === 'composite' && col.compositeFields?.length) {
150
+ javaType = toPascalCase(col.pgType)
151
+ } else {
152
+ const mapped = pgToJava(col.pgType, col.nullable, col.category)
153
+ javaType = mapped.type
154
+ for (const imp of mapped.imports) allImports.add(imp)
155
+ }
156
+
157
+ const annotations: string[] = []
158
+
159
+ // PK annotations
160
+ if (col.isPrimaryKey) {
161
+ annotations.push(' @Id')
162
+ if (col.isSerial) {
163
+ annotations.push(' @GeneratedValue(strategy = GenerationType.IDENTITY)')
164
+ }
165
+ }
166
+
167
+ // Enum annotation
168
+ if (col.category === 'enum' && col.enumValues?.length) {
169
+ annotations.push(' @Enumerated(EnumType.STRING)')
170
+ }
171
+
172
+ // Composite annotation
173
+ if (col.category === 'composite' && col.compositeFields?.length) {
174
+ annotations.push(' @Embedded')
175
+ }
176
+
177
+ // FK annotations
178
+ if (col.foreignKey) {
179
+ annotations.push(` @ManyToOne`)
180
+ annotations.push(` @JoinColumn(name = "${col.name}")`)
181
+ }
182
+
183
+ // Column annotations
184
+ const colAnnotationParts: string[] = []
185
+ if (!col.nullable && !col.isPrimaryKey) {
186
+ colAnnotationParts.push('nullable = false')
187
+ }
188
+ const varcharLen = getVarcharLength(col.pgType)
189
+ if (varcharLen) {
190
+ colAnnotationParts.push(`length = ${varcharLen}`)
191
+ }
192
+ if (colAnnotationParts.length > 0) {
193
+ annotations.push(` @Column(${colAnnotationParts.join(', ')})`)
194
+ }
195
+
196
+ // Validation annotations from @validate tags
197
+ const validation = getValidationAnnotations(col.tags)
198
+ for (const ann of validation.annotations) {
199
+ annotations.push(` ${ann}`)
200
+ }
201
+ for (const imp of validation.imports) {
202
+ allImports.add(imp)
203
+ }
204
+
205
+ if (annotations.length > 0) {
206
+ fieldLines.push(annotations.join('\n'))
207
+ }
208
+ fieldLines.push(` private ${javaType} ${col.camelName};`)
209
+ fieldLines.push('')
210
+ }
211
+
212
+ const sortedImports = [...allImports].sort()
213
+ const importLines = sortedImports.map((imp) => `import ${imp};`)
214
+
215
+ const parts: string[] = []
216
+ parts.push(importLines.join('\n'))
217
+ parts.push('')
218
+ parts.push('@Entity')
219
+ parts.push(`@Table(name = "${table.name}")`)
220
+ parts.push(`public class ${table.pascalName} {`)
221
+ parts.push('')
222
+ parts.push(fieldLines.join('\n'))
223
+ parts.push('}')
224
+ parts.push('')
225
+
226
+ files.push({
227
+ path: `${table.pascalName}.java`,
228
+ content: parts.join('\n'),
229
+ })
230
+ }
231
+
232
+ // Views (read-only — mapped as entities with a comment noting immutability)
233
+ for (const view of schema.views.filter((v) => !v.skipped)) {
234
+ const allImports = new Set<string>()
235
+ allImports.add('jakarta.persistence.*')
236
+
237
+ const fieldLines: string[] = []
238
+ for (const col of view.columns) {
239
+ let javaType: string
240
+ if (col.typeOverride) {
241
+ javaType = col.typeOverride
242
+ } else if (col.category === 'enum' && col.enumValues?.length) {
243
+ javaType = toPascalCase(col.pgType)
244
+ } else if (col.category === 'composite' && col.compositeFields?.length) {
245
+ javaType = toPascalCase(col.pgType)
246
+ } else {
247
+ const mapped = pgToJava(col.pgType, col.nullable, col.category)
248
+ javaType = mapped.type
249
+ for (const imp of mapped.imports) allImports.add(imp)
250
+ }
251
+
252
+ fieldLines.push(` private ${javaType} ${col.camelName};`)
253
+ fieldLines.push('')
254
+ }
255
+
256
+ const sortedImports = [...allImports].sort()
257
+ const importLines = sortedImports.map((imp) => `import ${imp};`)
258
+
259
+ const parts: string[] = []
260
+ parts.push(importLines.join('\n'))
261
+ parts.push('')
262
+ parts.push('/** Read-only (from view) */')
263
+ parts.push('@Entity')
264
+ parts.push(`@Table(name = "${view.name}")`)
265
+ parts.push(`public class ${view.pascalName} {`)
266
+ parts.push('')
267
+ parts.push(fieldLines.join('\n'))
268
+ parts.push('}')
269
+ parts.push('')
270
+
271
+ files.push({
272
+ path: `${view.pascalName}.java`,
273
+ content: parts.join('\n'),
274
+ })
275
+ }
276
+
277
+ return { files }
278
+ },
279
+ })
@@ -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,14 @@
1
+ FROM eclipse-temurin:21-jdk-alpine
2
+ WORKDIR /app
3
+ RUN apk add --no-cache curl
4
+ # Download Jakarta Persistence API JARs and PostgreSQL JDBC driver
5
+ RUN mkdir -p /deps && \
6
+ curl -sL -o /deps/jakarta.persistence-api-3.2.0.jar https://repo1.maven.org/maven2/jakarta/persistence/jakarta.persistence-api/3.2.0/jakarta.persistence-api-3.2.0.jar && \
7
+ curl -sL -o /deps/jakarta.validation-api-3.1.0.jar https://repo1.maven.org/maven2/jakarta/validation/jakarta.validation-api/3.1.0/jakarta.validation-api-3.1.0.jar && \
8
+ curl -sL -o /deps/jakarta.annotation-api-3.0.0.jar https://repo1.maven.org/maven2/jakarta/annotation/jakarta.annotation-api/3.0.0/jakarta.annotation-api-3.0.0.jar && \
9
+ curl -sL -o /deps/postgresql-42.7.4.jar https://repo1.maven.org/maven2/org/postgresql/postgresql/42.7.4/postgresql-42.7.4.jar
10
+ COPY . .
11
+ # Step 1: compile the generated JPA entities + test
12
+ RUN javac -cp "/deps/*:." *.java Test.java
13
+ # Step 2: run integration test against real DB
14
+ CMD ["java", "-cp", "/deps/*:.", "Test"]
@@ -0,0 +1,111 @@
1
+ import java.sql.*;
2
+ import java.lang.reflect.Field;
3
+
4
+ /**
5
+ * Integration test for @sqldoc/templates/jpa
6
+ * Verifies generated JPA entities compile, have expected fields, and DB has expected data.
7
+ */
8
+ public class Test {
9
+ static int failed = 0;
10
+
11
+ static void assertEq(Object actual, Object expected, String msg) {
12
+ if (!actual.equals(expected)) {
13
+ System.err.printf("FAIL: %s (got %s, expected %s)%n", msg, actual, expected);
14
+ failed++;
15
+ } else {
16
+ System.out.printf(" ok: %s%n", msg);
17
+ }
18
+ }
19
+
20
+ static void assertHasField(Class<?> cls, String fieldName, String msg) {
21
+ try {
22
+ cls.getDeclaredField(fieldName);
23
+ System.out.printf(" ok: %s%n", msg);
24
+ } catch (NoSuchFieldException e) {
25
+ System.err.printf("FAIL: %s (field '%s' not found)%n", msg, fieldName);
26
+ failed++;
27
+ }
28
+ }
29
+
30
+ public static void main(String[] args) throws Exception {
31
+ String dbUrl = System.getenv("DATABASE_URL");
32
+ if (dbUrl == null || dbUrl.isEmpty()) {
33
+ System.err.println("DATABASE_URL not set");
34
+ System.exit(1);
35
+ }
36
+
37
+ // Convert postgres(ql):// to jdbc:postgresql://, extracting userinfo for JDBC
38
+ var uri = java.net.URI.create(dbUrl.replaceFirst("^postgres(ql)?://", "http://"));
39
+ var userInfo = uri.getUserInfo();
40
+ var jdbcUrl = "jdbc:postgresql://" + uri.getHost() + ":" + (uri.getPort() > 0 ? uri.getPort() : 5432) + uri.getPath();
41
+ var query = uri.getQuery();
42
+ if (userInfo != null) {
43
+ var parts = userInfo.split(":", 2);
44
+ var sep = query != null ? "&" : "?";
45
+ jdbcUrl += (query != null ? "?" + query : "") + sep + "user=" + parts[0] + "&password=" + (parts.length > 1 ? parts[1] : "");
46
+ } else if (query != null) {
47
+ jdbcUrl += "?" + query;
48
+ }
49
+ dbUrl = jdbcUrl;
50
+
51
+ System.out.println("--- jpa integration test ---");
52
+
53
+ // 1. Verify generated entity classes have expected fields
54
+ assertHasField(Users.class, "id", "Users has 'id' field");
55
+ assertHasField(Users.class, "email", "Users has 'email' field");
56
+ assertHasField(Users.class, "name", "Users has 'name' field");
57
+ assertHasField(Users.class, "isActive", "Users has 'isActive' field");
58
+ assertHasField(Posts.class, "title", "Posts has 'title' field");
59
+ assertHasField(Posts.class, "viewCount", "Posts has 'viewCount' field");
60
+
61
+ // 2. Instantiate entity and populate via reflection (JPA entities have private fields)
62
+ try (Connection conn = DriverManager.getConnection(dbUrl)) {
63
+ try (PreparedStatement ps = conn.prepareStatement(
64
+ "SELECT id, email, name, age, is_active FROM users WHERE id = 1")) {
65
+ ResultSet rs = ps.executeQuery();
66
+ rs.next();
67
+ var user = new Users();
68
+ setField(user, "id", rs.getLong("id"));
69
+ setField(user, "email", rs.getString("email"));
70
+ setField(user, "name", rs.getString("name"));
71
+ setField(user, "age", rs.getObject("age") != null ? rs.getInt("age") : null);
72
+ setField(user, "isActive", rs.getBoolean("is_active"));
73
+
74
+ assertEq(getField(user, "email"), "test@example.com", "user.email matches");
75
+ assertEq(getField(user, "name"), "Test User", "user.name matches");
76
+ assertEq(getField(user, "isActive"), true, "user.isActive matches");
77
+ }
78
+
79
+ try (PreparedStatement ps = conn.prepareStatement(
80
+ "SELECT id, title, view_count FROM posts WHERE id = 1")) {
81
+ ResultSet rs = ps.executeQuery();
82
+ rs.next();
83
+ var post = new Posts();
84
+ setField(post, "id", rs.getLong("id"));
85
+ setField(post, "title", rs.getString("title"));
86
+ setField(post, "viewCount", rs.getInt("view_count"));
87
+
88
+ assertEq(getField(post, "title"), "Hello World", "post.title matches");
89
+ assertEq(getField(post, "viewCount"), 42, "post.viewCount matches");
90
+ }
91
+ }
92
+
93
+ if (failed > 0) {
94
+ System.err.printf("%n%d assertion(s) failed%n", failed);
95
+ System.exit(1);
96
+ }
97
+ System.out.println("\nAll assertions passed!");
98
+ }
99
+
100
+ static void setField(Object obj, String name, Object value) throws Exception {
101
+ Field f = obj.getClass().getDeclaredField(name);
102
+ f.setAccessible(true);
103
+ f.set(obj, value);
104
+ }
105
+
106
+ static Object getField(Object obj, String name) throws Exception {
107
+ Field f = obj.getClass().getDeclaredField(name);
108
+ f.setAccessible(true);
109
+ return f.get(obj);
110
+ }
111
+ }