Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ dependencies {
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2',
'io.jsonwebtoken:jjwt-jackson:0.11.2'
implementation 'joda-time:joda-time:2.10.13'
implementation 'org.xerial:sqlite-jdbc:3.36.0.3'
runtimeOnly 'org.xerial:sqlite-jdbc:3.36.0.3'
runtimeOnly 'org.postgresql:postgresql'

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
Expand All @@ -54,6 +55,9 @@ dependencies {
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:2.2.2'
testImplementation 'org.testcontainers:testcontainers:1.16.3'
testImplementation 'org.testcontainers:postgresql:1.16.3'
testImplementation 'org.testcontainers:junit-jupiter:1.16.3'
}

tasks.named('test') {
Expand Down
28 changes: 28 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
version: '3.8'

services:
postgresql:
image: postgres:14-alpine
environment:
POSTGRES_DB: realworld
POSTGRES_USER: realworld
POSTGRES_PASSWORD: realworld
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data

app:
build: .
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: prod
DATABASE_URL: jdbc:postgresql://postgresql:5432/realworld
DATABASE_USERNAME: realworld
DATABASE_PASSWORD: realworld
depends_on:
- postgresql

volumes:
postgres_data:
6 changes: 6 additions & 0 deletions src/main/resources/application-dev.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
spring.datasource.url=jdbc:sqlite:dev.db
spring.datasource.driver-class-name=org.sqlite.JDBC
spring.datasource.username=
spring.datasource.password=

spring.flyway.locations=classpath:db/migration/sqlite
6 changes: 6 additions & 0 deletions src/main/resources/application-prod.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
spring.datasource.url=${DATABASE_URL:jdbc:postgresql://localhost:5432/realworld}
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.username=${DATABASE_USERNAME:realworld}
spring.datasource.password=${DATABASE_PASSWORD:realworld}

spring.flyway.locations=classpath:db/migration/postgresql
7 changes: 6 additions & 1 deletion src/main/resources/application-test.properties
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
spring.datasource.url=jdbc:sqlite::memory:
spring.datasource.url=jdbc:tc:postgresql:14-alpine:///realworld_test
spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.datasource.username=test
spring.datasource.password=test

spring.flyway.locations=classpath:db/migration/postgresql
6 changes: 2 additions & 4 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
spring.datasource.url=jdbc:sqlite:dev.db
spring.datasource.driver-class-name=org.sqlite.JDBC
spring.datasource.username=
spring.datasource.password=
spring.profiles.active=${SPRING_PROFILES_ACTIVE:dev}

spring.jackson.deserialization.UNWRAP_ROOT_VALUE=true

image.default=https://static.productionready.io/images/smiley-cyrus.jpg
Expand Down
49 changes: 49 additions & 0 deletions src/main/resources/db/migration/postgresql/V1__create_tables.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
CREATE TABLE users (
id VARCHAR(255) PRIMARY KEY,
username VARCHAR(255) UNIQUE,
password VARCHAR(255),
email VARCHAR(255) UNIQUE,
bio TEXT,
image VARCHAR(511)
);

CREATE TABLE articles (
id VARCHAR(255) PRIMARY KEY,
user_id VARCHAR(255),
slug VARCHAR(255) UNIQUE,
title VARCHAR(255),
description TEXT,
body TEXT,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE article_favorites (
article_id VARCHAR(255) NOT NULL,
user_id VARCHAR(255) NOT NULL,
PRIMARY KEY(article_id, user_id)
);

CREATE TABLE follows (
user_id VARCHAR(255) NOT NULL,
follow_id VARCHAR(255) NOT NULL
);

CREATE TABLE tags (
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255) NOT NULL
);

CREATE TABLE article_tags (
article_id VARCHAR(255) NOT NULL,
tag_id VARCHAR(255) NOT NULL
);

CREATE TABLE comments (
id VARCHAR(255) PRIMARY KEY,
body TEXT,
article_id VARCHAR(255),
user_id VARCHAR(255),
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
6 changes: 3 additions & 3 deletions src/main/resources/mapper/ArticleMapper.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@
T.id tagId,
T.name tagName
from articles A
left join article_tags AT on A.id = AT.article_id
left join tags T on T.id = AT.tag_id
left join article_tags ATAG on A.id = ATAG.article_id
left join tags T on T.id = ATAG.tag_id
</sql>

<select id="findById" resultMap="article">
Expand Down Expand Up @@ -79,4 +79,4 @@
<id column="tagId" property="id"/>
<result column="tagName" property="name"/>
</resultMap>
</mapper>
</mapper>
14 changes: 7 additions & 7 deletions src/main/resources/mapper/ArticleReadService.xml
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 countArticle query still uses reserved keyword AT as table alias, breaking PostgreSQL

The countArticle query in ArticleReadService.xml at lines 68-69 still uses AT as the alias for article_tags. While the PR renamed ATATAG in the selectArticleData and selectArticleIds SQL fragments, the countArticle query has its own inline join that was missed.

Root Cause and Impact

AT is a reserved keyword in PostgreSQL. The other queries in this file were correctly updated to use ATAG (lines 23, 24, 32, 33), but the countArticle query at lines 68-69 was overlooked:

left join article_tags AT on A.id = AT.article_id
left join tags T on T.id = AT.tag_id

This query is used to count the total number of articles matching filter criteria (by tag, author, or favorited-by). When running against PostgreSQL (both prod and test profiles), this query will fail with a syntax error because AT is a reserved keyword.

Impact: The article listing/filtering endpoints that call countArticle (to return total counts for pagination) will throw SQL exceptions on PostgreSQL, making pagination broken in production and tests.

(Refers to lines 68-69)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@
<include refid="profileColumns"/>
from
articles A
left join article_tags AT on A.id = AT.article_id
left join tags T on T.id = AT.tag_id
left join article_tags ATAG on A.id = ATAG.article_id
left join tags T on T.id = ATAG.tag_id
left join users U on U.id = A.user_id
</sql>
<sql id="selectArticleIds">
select
DISTINCT(A.id) articleId, A.created_at
from
articles A
left join article_tags AT on A.id = AT.article_id
left join tags T on T.id = AT.tag_id
left join article_tags ATAG on A.id = ATAG.article_id
left join tags T on T.id = ATAG.tag_id
left join article_favorites AF on AF.article_id = A.id
left join users AU on AU.id = A.user_id
left join users AFU on AFU.id = AF.user_id
Expand Down Expand Up @@ -58,7 +58,7 @@
</if>
</where>
order by A.created_at desc
limit #{page.offset}, #{page.limit}
limit #{page.limit} offset #{page.offset}
</select>
<select id="countArticle" resultType="java.lang.Integer">
select
Expand Down Expand Up @@ -96,7 +96,7 @@
<foreach index="index" collection="authors" item="id" open="(" separator="," close=")">
#{id}
</foreach>
limit #{page.offset}, #{page.limit}
limit #{page.limit} offset #{page.offset}
</select>
<select id="countFeedSize" resultType="java.lang.Integer">
select count(1) from articles A where A.user_id in
Expand Down Expand Up @@ -157,4 +157,4 @@
<resultMap id="articleId" type="string">
<id javaType="string" column="articleId"/>
</resultMap>
</mapper>
</mapper>
12 changes: 12 additions & 0 deletions src/test/java/io/spring/infrastructure/DbTestBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,20 @@
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;

@ActiveProfiles("test")
@AutoConfigureTestDatabase(replace = Replace.NONE)
@MybatisTest
@Sql(
statements = {
"DELETE FROM article_favorites",
"DELETE FROM article_tags",
"DELETE FROM comments",
"DELETE FROM follows",
"DELETE FROM articles",
"DELETE FROM tags",
"DELETE FROM users"
},
executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
public abstract class DbTestBase {}
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,22 @@
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;

@ActiveProfiles("test")
@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Sql(
statements = {
"DELETE FROM article_favorites",
"DELETE FROM article_tags",
"DELETE FROM comments",
"DELETE FROM follows",
"DELETE FROM articles",
"DELETE FROM tags",
"DELETE FROM users"
},
executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
public class ArticleRepositoryTransactionTest {
@Autowired private ArticleRepository articleRepository;

Expand Down