From 59b80e633f0d515a24094617760f38d7d907f9c4 Mon Sep 17 00:00:00 2001 From: Yeongseon Choe Date: Sat, 4 Apr 2026 11:25:19 +0000 Subject: [PATCH 1/3] feat: add make verify infrastructure with expected outputs for all 12 examples - Add scripts/normalize_output.sh for reproducible output comparison - Add expected output files for 6 pycubrid and 6 SQLAlchemy examples - Add verify and demo targets to Makefile (Python-only) - Fix non-deterministic sort order in 04_relationships.py - Simplify GETTING_STARTED.md to under 20 lines with 3-line quick start Closes #8 (reimplemented from scratch after branch deletion) Partial #7 (verify infrastructure + fundamentals expected outputs) --- GETTING_STARTED.md | 317 +----------------- Makefile | 41 ++- .../connect/expected/01_connect.expected | 19 ++ fundamentals/crud/expected/02_crud.expected | 48 +++ .../expected/05_error_handling.expected | 38 +++ .../lob-handling/expected/06_lob.expected | 17 + fundamentals/orm-basics/04_relationships.py | 6 +- .../orm-basics/expected/01_engine.expected | 37 ++ .../orm-basics/expected/02_core.expected | 49 +++ .../orm-basics/expected/03_orm.expected | 58 ++++ .../expected/04_relationships.expected | 37 ++ .../expected/05_dml_extensions.expected | 36 ++ .../expected/06_reflection.expected | 47 +++ .../expected/04_prepared.expected | 25 ++ .../expected/03_transactions.expected | 25 ++ scripts/normalize_output.sh | 29 ++ 16 files changed, 519 insertions(+), 310 deletions(-) create mode 100644 fundamentals/connect/expected/01_connect.expected create mode 100644 fundamentals/crud/expected/02_crud.expected create mode 100644 fundamentals/error-handling/expected/05_error_handling.expected create mode 100644 fundamentals/lob-handling/expected/06_lob.expected create mode 100644 fundamentals/orm-basics/expected/01_engine.expected create mode 100644 fundamentals/orm-basics/expected/02_core.expected create mode 100644 fundamentals/orm-basics/expected/03_orm.expected create mode 100644 fundamentals/orm-basics/expected/04_relationships.expected create mode 100644 fundamentals/orm-basics/expected/05_dml_extensions.expected create mode 100644 fundamentals/orm-basics/expected/06_reflection.expected create mode 100644 fundamentals/prepared-statements/expected/04_prepared.expected create mode 100644 fundamentals/transactions/expected/03_transactions.expected create mode 100755 scripts/normalize_output.sh diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index d169238..96997c6 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -1,313 +1,18 @@ -# CUBRID in 5 Minutes โ€” Getting Started Guide - -> Pick your language. Connect to CUBRID. Build something. All in 5 minutes. - -This guide gets you from zero to a working CUBRID application in your language of choice. No prior CUBRID experience needed. - -## Prerequisites - -```bash -# Start a CUBRID database with Docker (one command) -docker run -d --name cubrid -p 33000:33000 -e CUBRID_DB=testdb cubrid/cubrid:11.2 -``` - -Wait ~10 seconds for startup, then choose your language below. - ---- - -## ๐Ÿ Python - -### Option A: SQLAlchemy ORM (Recommended) - -```bash -pip install sqlalchemy-cubrid[pycubrid] -``` - -```python -from sqlalchemy import create_engine, String -from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column - -class Base(DeclarativeBase): - pass - -class User(Base): - __tablename__ = "users" - id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - name: Mapped[str] = mapped_column(String(100)) - email: Mapped[str] = mapped_column(String(200), unique=True) - -engine = create_engine("cubrid+pycubrid://dba@localhost:33000/testdb") -Base.metadata.create_all(engine) - -with Session(engine) as session: - # Create - user = User(name="Alice", email="alice@example.com") - session.add(user) - session.commit() - - # Read - users = session.query(User).all() - for u in users: - print(f"{u.name} ({u.email})") -``` - -### Option B: Direct Driver - -```bash -pip install pycubrid -``` - -```python -import pycubrid - -conn = pycubrid.connect(host="localhost", port=33000, database="testdb", user="dba") -cursor = conn.cursor() - -cursor.execute(""" - CREATE TABLE IF NOT EXISTS products ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(100) NOT NULL, - price DOUBLE - ) -""") - -cursor.execute("INSERT INTO products (name, price) VALUES (?, ?)", ("Widget", 9.99)) -conn.commit() - -cursor.execute("SELECT * FROM products") -for row in cursor.fetchall(): - print(row) - -cursor.close() -conn.close() -``` - -**More**: [Full Python examples โ†’](python/) - ---- - -## ๐ŸŸฆ TypeScript / Node.js - -### Option A: Drizzle ORM (Recommended) - -```bash -npm install drizzle-cubrid drizzle-orm cubrid-client -``` - -```typescript -import { createClient } from "cubrid-client"; -import { drizzle, cubridTable, varchar, integer, serial, sql } from "drizzle-cubrid"; - -// Define schema -const users = cubridTable("users", { - id: serial("id").primaryKey(), - name: varchar("name", { length: 100 }).notNull(), - email: varchar("email", { length: 200 }).unique(), -}); - -// Connect -const client = createClient({ - host: "localhost", - port: 33000, - database: "testdb", - user: "dba", -}); -const db = drizzle(client); - -// Create table -await db.execute(sql`CREATE TABLE IF NOT EXISTS users ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(100) NOT NULL, - email VARCHAR(200) UNIQUE -)`); - -// Insert -await db.insert(users).values({ name: "Alice", email: "alice@example.com" }); - -// Query -const allUsers = await db.select().from(users); -console.log(allUsers); - -await client.close(); -``` - -### Option B: Direct Client - -```bash -npm install cubrid-client -``` - -```typescript -import { createClient } from "cubrid-client"; - -const db = createClient({ - host: "localhost", - port: 33000, - database: "testdb", - user: "dba", -}); - -type User = { id: number; name: string; email: string }; - -// Typed query results -const users = await db.query("SELECT * FROM users WHERE name = ?", ["Alice"]); -console.log(users); // User[] with full type safety - -// Transactions with auto-rollback -await db.transaction(async (tx) => { - await tx.query("INSERT INTO orders (item) VALUES (?)", ["Widget"]); - await tx.query("UPDATE inventory SET qty = qty - 1 WHERE item = ?", ["Widget"]); -}); - -await db.close(); -``` - -**More**: [Full Node.js examples โ†’](node/) - ---- - -## ๐Ÿน Go - -### Option A: GORM (Recommended) - -```bash -go get github.com/cubrid-labs/cubrid-go -``` - -```go -package main - -import ( - "fmt" - "log" - - "gorm.io/gorm" - cubrid "github.com/cubrid-labs/cubrid-go/dialector" -) - -type User struct { - ID int `gorm:"primaryKey;autoIncrement"` - Name string `gorm:"size:100;not null"` - Email string `gorm:"size:200;uniqueIndex"` -} - -func main() { - db, err := gorm.Open(cubrid.Open("cubrid://dba:@localhost:33000/testdb"), &gorm.Config{}) - if err != nil { - log.Fatal(err) - } - - // Auto-create table - db.AutoMigrate(&User{}) - - // Create - db.Create(&User{Name: "Alice", Email: "alice@example.com"}) - - // Read - var users []User - db.Find(&users) - for _, u := range users { - fmt.Printf("%s (%s)\n", u.Name, u.Email) - } -} -``` - -### Option B: database/sql +# Getting Started ```bash -go get github.com/cubrid-labs/cubrid-go -``` - -```go -package main - -import ( - "database/sql" - "fmt" - "log" - - _ "github.com/cubrid-labs/cubrid-go" -) - -func main() { - db, err := sql.Open("cubrid", "cubrid://dba:@localhost:33000/testdb") - if err != nil { - log.Fatal(err) - } - defer db.Close() - - // Verify connection - if err := db.Ping(); err != nil { - log.Fatal(err) - } - - // Query with parameters - rows, err := db.Query("SELECT name, email FROM users WHERE name = ?", "Alice") - if err != nil { - log.Fatal(err) - } - defer rows.Close() - - for rows.Next() { - var name, email string - rows.Scan(&name, &email) - fmt.Printf("%s (%s)\n", name, email) - } -} +docker compose up -d # Start CUBRID 11.2 +pip install pycubrid sqlalchemy-cubrid +make verify # Run all examples and check outputs ``` -**More**: [Full Go examples โ†’](go/) - ---- - -## What's Next? +## Quick Links -| Want to... | Go here | +| Section | Description | |---|---| -| Build a REST API | [FastAPI example](python/fastapi/) ยท [Flask example](python/flask/) | -| Use an ORM | [SQLAlchemy](python/sqlalchemy/) ยท [Drizzle](node/drizzle/) ยท [GORM](go/gorm/) | -| Analyze data | [Pandas example](python/pandas/) | -| Build a dashboard | [Streamlit example](python/streamlit/) | -| Run background tasks | [Celery example](python/celery/) | -| Use Django | [Django example](python/django/) | - -## CUBRID vs. Other Databases - -| Feature | CUBRID | MySQL | PostgreSQL | -|---|---|---|---| -| Open source | โœ… MIT-like | โœ… GPL | โœ… PostgreSQL | -| ACID transactions | โœ… | โœ… | โœ… | -| MVCC | โœ… | โœ… (InnoDB) | โœ… | -| Collection types (SET) | โœ… Native | โŒ | โœ… (ARRAY) | -| JSON support | โœ… (10.2+) | โœ… | โœ… | -| Window functions | โœ… | โœ… (8.0+) | โœ… | -| Docker image | โœ… `cubrid/cubrid` | โœ… | โœ… | -| Default port | 33000 | 3306 | 5432 | - -## Cleanup - -```bash -docker stop cubrid && docker rm cubrid -``` - ---- - -## Ecosystem at a Glance - -```mermaid -flowchart TD - C[(CUBRID Database
cubrid/cubrid:11.2
TCP :33000)] - - PY[Python] --> PYD[pycubrid (DB-API)] --> PYO[sqlalchemy-cubrid (ORM)] --> C - TS[TypeScript] --> TSD[cubrid-client (Native)] --> TSO[drizzle-cubrid (ORM)] --> C - GO[Go] --> GOD[cubrid-go (database/sql)] --> GOO[GORM dialector (ORM)] --> C - - PY --> PYF[FastAPI] - PY --> PYJ[Django] - PY --> PYL[Flask] - PY --> PYP[Pandas] - PY --> PYS[Streamlit] - PY --> PYC[Celery] -``` +| [fundamentals/connect/](fundamentals/connect/) | Connection basics | +| [fundamentals/crud/](fundamentals/crud/) | Create, read, update, delete | +| [fundamentals/transactions/](fundamentals/transactions/) | Commit, rollback, savepoints | +| [fundamentals/orm-basics/](fundamentals/orm-basics/) | SQLAlchemy ORM with CUBRID | -All packages: [github.com/cubrid-labs](https://github.com/cubrid-labs) +See [README.md](README.md) for the full example catalog. diff --git a/Makefile b/Makefile index 4fccb92..9a8eb13 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,9 @@ -.PHONY: help up down status clean +SHELL := /bin/bash +.PHONY: help up down status clean verify demo DOCKER_COMPOSE := docker compose +NORMALIZE := bash scripts/normalize_output.sh +PYTHON := python3 help: ## Show this help @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \ @@ -23,3 +26,39 @@ status: ## Show container status clean: ## Stop and remove all data $(DOCKER_COMPOSE) down -v @echo "โœ“ Cleaned up all containers and volumes" + +verify: ## Verify example outputs against expected results + @echo "Verifying example outputs..." + @PASS=0; FAIL=0; SKIP=0; \ + for expected in $$(find . -path '*/expected/*.expected' | sort); do \ + dir=$$(dirname "$$(dirname "$$expected")"); \ + base=$$(basename "$$expected" .expected); \ + script="$$dir/$$base.py"; \ + if [ ! -f "$$script" ]; then \ + echo " ? SKIP $$expected (no matching script)"; \ + SKIP=$$((SKIP + 1)); \ + continue; \ + fi; \ + actual=$$(set -o pipefail; $(PYTHON) "$$script" 2>&1 | $(NORMALIZE)); \ + if [ $$? -ne 0 ]; then \ + echo " โœ— FAIL $$script (script error)"; \ + FAIL=$$((FAIL + 1)); \ + continue; \ + fi; \ + expected_content=$$(cat "$$expected"); \ + if [ "$$actual" = "$$expected_content" ]; then \ + echo " โœ“ PASS $$script"; \ + PASS=$$((PASS + 1)); \ + else \ + echo " โœ— FAIL $$script"; \ + diff <(echo "$$actual") <(echo "$$expected_content") || true; \ + FAIL=$$((FAIL + 1)); \ + fi; \ + done; \ + echo ""; \ + echo "Results: $$PASS passed, $$FAIL failed, $$SKIP skipped"; \ + [ "$$FAIL" -eq 0 ] + +demo: up verify ## Full demo: start DB, verify all examples + @echo "" + @echo "โœ“ Demo complete โ€” all examples verified against CUBRID" diff --git a/fundamentals/connect/expected/01_connect.expected b/fundamentals/connect/expected/01_connect.expected new file mode 100644 index 0000000..dcc86a4 --- /dev/null +++ b/fundamentals/connect/expected/01_connect.expected @@ -0,0 +1,19 @@ +=== Basic Connection === +1 + 1 = 2 + +=== Connection Info === +CUBRID version: {{VERSION}} +Database: testdb +User: DBA@{{HOSTNAME}} + +=== Cursor Description === +Columns: + id type_code=8 + name type_code=1 + val type_code=12 +Row: (1, 'hello', 3.14) + +=== Multiple Queries === +SELECT 1 AS a โ†’ 1 +SELECT 'hello' AS b โ†’ hello +SELECT CURRENT_DATE AS today โ†’ {{DATE}} diff --git a/fundamentals/crud/expected/02_crud.expected b/fundamentals/crud/expected/02_crud.expected new file mode 100644 index 0000000..f748d0d --- /dev/null +++ b/fundamentals/crud/expected/02_crud.expected @@ -0,0 +1,48 @@ +โœ“ Created table 'cookbook_users' +โœ“ Inserted 5 rows + +All users (5 rows): + ID Name Email Age + --- ---- ----- --- + 1 Alice alice@example.com 30 + 2 Bob bob@example.com 25 + 3 Charlie charlie@example.com 35 + 4 Diana diana@example.com 28 + 5 Eve eve@example.com 32 + +Users age >= 30 (3 rows): + Charlie age=35 + Eve age=32 + Alice age=30 + +Users with 'li' in name (2 rows): + Alice alice@example.com + Charlie charlie@example.com + +fetchone(): Alice +fetchmany(2): ['Bob', 'Charlie'] +fetchall(): ['Diana', 'Eve'] + +โœ“ Updated Alice's age (rows affected: 1) +โœ“ Incremented age for young users (rows affected: 2) + +All users (5 rows): + ID Name Email Age + --- ---- ----- --- + 1 Alice alice@example.com 31 + 2 Bob bob@example.com 26 + 3 Charlie charlie@example.com 35 + 4 Diana diana@example.com 29 + 5 Eve eve@example.com 32 + +โœ“ Deleted Eve (rows affected: 1) + +All users (4 rows): + ID Name Email Age + --- ---- ----- --- + 1 Alice alice@example.com 31 + 2 Bob bob@example.com 26 + 3 Charlie charlie@example.com 35 + 4 Diana diana@example.com 29 + +โœ“ Cleaned up table 'cookbook_users' diff --git a/fundamentals/error-handling/expected/05_error_handling.expected b/fundamentals/error-handling/expected/05_error_handling.expected new file mode 100644 index 0000000..d4d3319 --- /dev/null +++ b/fundamentals/error-handling/expected/05_error_handling.expected @@ -0,0 +1,38 @@ +=== PEP 249 Exception Hierarchy === + Exception + โ”œโ”€โ”€ Warning + โ””โ”€โ”€ Error + โ”œโ”€โ”€ InterfaceError + โ””โ”€โ”€ DatabaseError + โ”œโ”€โ”€ DataError + โ”œโ”€โ”€ OperationalError + โ”œโ”€โ”€ IntegrityError + โ”œโ”€โ”€ InternalError + โ”œโ”€โ”€ ProgrammingError + โ””โ”€โ”€ NotSupportedError + โœ“ Hierarchy verified + +=== Connection Error Handling === + โœ“ Caught OperationalError (bad host): failed to connect to CUBRID broker + โœ“ Caught connection error (bad port): OperationalError: failed to connect to CUBRID broker + +=== SQL Syntax Errors === + โœ“ Caught ProgrammingError (bad SQL): Syntax: In line {{LINE}}, column {{COL}} before ' * FORM nonexistent_table' +Syntax error: unexpected 'SELEC', expecting SELECT or VALUE or VALUES or '(' + +=== Integrity Errors === + โœ“ Caught IntegrityError (duplicate PK): Operation would have caused one or more unique constraint violations. INDEX pk_cookbook_err_test_id(B+tree: {{BTREE}}) ON CLASS dba.cookbook_err_test(CLASS_OID: {{OID}}). key: 1(OID: {{OID}}). + โœ“ Caught IntegrityError (duplicate UNIQUE): Operation would have caused one or more unique constraint violations. INDEX u_cookbook_err_test_email(B+tree: {{BTREE}}) ON CLASS dba.cookbook_err_test(CLASS_OID: {{OID}}). key: 'a@test.com'(OID: {{OID}}). + +=== Error Recovery Pattern === + โœ“ Results: 4 succeeded, 0 failed + Widget: $10.00 + Gadget: $25.00 + None: $-1.00 + Doohickey: $15.00 + +=== Generic Error Catching === + โœ“ 'SELECT 1' โ†’ (1,) + โœ— 'SELECT * FROM this_table_does_not_exist' โ†’ ProgrammingError: Syntax: Unknown class "dba.this_table_does_not_exist". select * from [dba.this_table_does_not_exist] + โœ— 'INVALID SQL SYNTAX HERE' โ†’ ProgrammingError: Syntax: In line {{LINE}}, column {{COL}} before ' SQL SYNTAX HERE' +Syntax error: unexpected 'INVALID', expecting SELECT or VALUE or VALUES or '(' diff --git a/fundamentals/lob-handling/expected/06_lob.expected b/fundamentals/lob-handling/expected/06_lob.expected new file mode 100644 index 0000000..1d23e35 --- /dev/null +++ b/fundamentals/lob-handling/expected/06_lob.expected @@ -0,0 +1,17 @@ +=== CLOB (Character Large Object) === + โœ“ Inserted 3 documents with CLOB data + README (64 chars): # CUBRID Cookbook + +A collection of examples for CUBRID datab... + License (46 chars): Apache License 2.0 + +Copyright 2026 cubrid-labs + Long Text (2,800 chars): Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lore... + +=== BLOB (Binary Large Object) === + โœ“ Inserted 3 files with BLOB data + icon.bin (256 bytes) + sample.bin (3,000 bytes) + empty.bin (NULL) + +โœ“ Cleaned up diff --git a/fundamentals/orm-basics/04_relationships.py b/fundamentals/orm-basics/04_relationships.py index 0b74c8c..77b3aba 100644 --- a/fundamentals/orm-basics/04_relationships.py +++ b/fundamentals/orm-basics/04_relationships.py @@ -162,7 +162,7 @@ def one_to_many(engine) -> None: professors = session.scalars(stmt).all() for prof in professors: - course_list = ", ".join(c.title for c in prof.courses) or "none" + course_list = ", ".join(sorted(c.title for c in prof.courses)) or "none" print(f" {prof.name} teaches: {course_list}") @@ -177,7 +177,7 @@ def many_to_many(engine) -> None: print(" Course enrollments:") for course in courses: - students = ", ".join(s.name for s in course.students) or "none" + students = ", ".join(sorted(s.name for s in course.students)) or "none" print(f" {course.title:25s} ({len(course.students)} students): {students}") # Student โ†’ Courses @@ -186,7 +186,7 @@ def many_to_many(engine) -> None: students = session.scalars(stmt).all() for student in students: - courses_str = ", ".join(c.title for c in student.courses) or "none" + courses_str = ", ".join(sorted(c.title for c in student.courses)) or "none" print(f" {student.name:10s} ({len(student.courses)} courses): {courses_str}") diff --git a/fundamentals/orm-basics/expected/01_engine.expected b/fundamentals/orm-basics/expected/01_engine.expected new file mode 100644 index 0000000..dabbaf6 --- /dev/null +++ b/fundamentals/orm-basics/expected/01_engine.expected @@ -0,0 +1,37 @@ +=== Basic Engine === + SELECT 1 + 1 = 2 + CUBRID version: {{VERSION}} + โœ“ Engine created and disposed + +=== Engine with Pool Settings === + Pool size: 5 + Checked in: 0 + Checked out: 0 + After connect โ€” checked out: 1 + After close โ€” checked out: 0 + โœ“ Pool engine created and disposed + +=== Engine with SQL Echo === + (SQL statements will be logged below) +{{TIMESTAMP}} INFO sqlalchemy.engine.Engine SELECT VERSION() +{{TIMESTAMP}} INFO sqlalchemy.engine.Engine [generated in {{TIME}}s] () +{{TIMESTAMP}} INFO sqlalchemy.engine.Engine SELECT SCHEMA() +{{TIMESTAMP}} INFO sqlalchemy.engine.Engine [generated in {{TIME}}s] () +{{TIMESTAMP}} INFO sqlalchemy.engine.Engine BEGIN (implicit) +{{TIMESTAMP}} INFO sqlalchemy.engine.Engine SELECT 'hello from CUBRID' +{{TIMESTAMP}} INFO sqlalchemy.engine.Engine [generated in {{TIME}}s] () +{{TIMESTAMP}} INFO sqlalchemy.engine.Engine ROLLBACK + โœ“ Echo mode demonstrated + +=== Engine Events === + Queries executed: 3 + โœ“ Event listeners demonstrated + +=== Supported URL Formats === + pycubrid (pure Python) cubrid+pycubrid://dba@localhost:33000/testdb + pycubrid with password cubrid+pycubrid://dba:password@localhost:33000/testdb + CUBRIDdb (C extension) cubrid://dba@localhost:33000/testdb + CUBRIDdb with password cubrid://dba:password@localhost:33000/testdb + + Active driver: pycubrid + Dialect: cubrid diff --git a/fundamentals/orm-basics/expected/02_core.expected b/fundamentals/orm-basics/expected/02_core.expected new file mode 100644 index 0000000..963fbb5 --- /dev/null +++ b/fundamentals/orm-basics/expected/02_core.expected @@ -0,0 +1,49 @@ +=== Create Tables === + โœ“ Created tables: cookbook_departments, cookbook_employees + +=== Core INSERT === + โœ“ Inserted 3 departments + โœ“ Inserted 5 employees + +=== Core SELECT === + All employees: + 1: Alice alice@company.com $95,000 + 2: Bob bob@company.com $85,000 + 3: Charlie charlie@company.com $75,000 + 4: Diana diana@company.com $90,000 + 5: Eve eve@company.com $80,000 + + High earners (>= $85k): + Alice $95,000 + Diana $90,000 + Bob $85,000 + +=== Core JOIN === + Employee Department Salary + -------- ---------- ------ + Alice Engineering $ 95,000 + Bob Engineering $ 85,000 + Charlie Marketing $ 75,000 + Diana Marketing $ 90,000 + Eve Sales $ 80,000 + +=== Core Aggregation === + Department Count Avg Salary Total + ---------- ----- ---------- ----- + Engineering 2 $ 90,000 $ 180,000 + Marketing 2 $ 82,500 $ 165,000 + Sales 1 $ 80,000 $ 80,000 + +=== Raw SQL with text() === + Total employees: 5 + Employees with salary > $80k: + Alice $95,000 + Diana $90,000 + Bob $85,000 + +=== Core UPDATE & DELETE === + โœ“ Gave Engineering 10% raise (2 rows) + Alice $104,500 + Bob $93,500 + +โœ“ Cleaned up diff --git a/fundamentals/orm-basics/expected/03_orm.expected b/fundamentals/orm-basics/expected/03_orm.expected new file mode 100644 index 0000000..8bfd094 --- /dev/null +++ b/fundamentals/orm-basics/expected/03_orm.expected @@ -0,0 +1,58 @@ +=== ORM โ€” Create Tables === + โœ“ Created 'cookbook_books' table + +=== ORM โ€” Add Books === + โœ“ Added 5 books + Book(id=1, title='Clean Code', author='Robert C. Martin') + Book(id=2, title='Design Patterns', author='Gang of Four') + Book(id=3, title='The Pragmatic Programmer', author='David Thomas') + Book(id=4, title='Refactoring', author='Martin Fowler') + Book(id=5, title='Clean Architecture', author='Robert C. Martin') + +=== ORM โ€” Query Books === + All books (5): + Clean Code by Robert C. Martin $39.99 + Design Patterns by Gang of Four $49.99 + The Pragmatic Programmer by David Thomas $44.99 + Refactoring by Martin Fowler $47.99 + Clean Architecture by Robert C. Martin $34.99 + + Books by Robert C. Martin (2): + Clean Code + Clean Architecture + + Books $40-$50 (3): + The Pragmatic Programmer $44.99 + Refactoring $47.99 + Design Patterns $49.99 + +=== ORM โ€” Advanced Queries === + Books by author: + Robert C. Martin 2 books avg $37.49 + David Thomas 1 books avg $44.99 + Gang of Four 1 books avg $49.99 + Martin Fowler 1 books avg $47.99 + + Page 1: + Clean Code + Design Patterns + + Page 2: + The Pragmatic Programmer + Refactoring + + Page 3: + Clean Architecture + + Total books: 5 + 'Clean Code' exists: True + +=== ORM โ€” Update Books === + โœ“ Updated 'Clean Code' price: $39.99 โ†’ $42.99 + โœ“ Applied 10% discount to 3 books with 400+ pages + +=== ORM โ€” Delete Books === + โœ“ Deleted 'Design Patterns' + Remaining books: 4 + +โœ“ Cleaned up diff --git a/fundamentals/orm-basics/expected/04_relationships.expected b/fundamentals/orm-basics/expected/04_relationships.expected new file mode 100644 index 0000000..92bc8fc --- /dev/null +++ b/fundamentals/orm-basics/expected/04_relationships.expected @@ -0,0 +1,37 @@ +=== Relationships โ€” Setup === + โœ“ Created tables with foreign keys + +=== Seed Data === + โœ“ Seeded 2 departments, 3 professors, 4 courses, 3 students + +=== One-to-Many === + Computer Science: + โ€ข Dr. Smith + โ€ข Dr. Jones + Mathematics: + โ€ข Dr. Taylor + + Dr. Smith teaches: Algorithms, Databases + Dr. Taylor teaches: Calculus + Dr. Jones teaches: Machine Learning + +=== Many-to-Many === + Course enrollments: + Algorithms (3 students): Alice, Bob, Charlie + Calculus (1 students): Alice + Machine Learning (2 students): Bob, Charlie + Databases (2 students): Alice, Charlie + + Student schedules: + Alice (3 courses): Algorithms, Calculus, Databases + Bob (2 courses): Algorithms, Machine Learning + Charlie (3 courses): Algorithms, Databases, Machine Learning + +=== Eager Loading === + Full course catalog: + Algorithms Prof: Dr. Smith Students: 3 + Calculus Prof: Dr. Taylor Students: 1 + Databases Prof: Dr. Smith Students: 2 + Machine Learning Prof: Dr. Jones Students: 2 + +โœ“ Cleaned up diff --git a/fundamentals/orm-basics/expected/05_dml_extensions.expected b/fundamentals/orm-basics/expected/05_dml_extensions.expected new file mode 100644 index 0000000..74c50bb --- /dev/null +++ b/fundamentals/orm-basics/expected/05_dml_extensions.expected @@ -0,0 +1,36 @@ +=== DML Extensions โ€” Setup === + โœ“ Created tables + +=== ON DUPLICATE KEY UPDATE === + Initial insert: + app.name = CUBRID Cookbook (Application name) + + After ODKU (updated value): + app.name = CUBRID Cookbook v2 (Updated application name) + + After ODKU (new key inserted): + app.name = CUBRID Cookbook v2 (Updated application name) + app.version = 1.0.0 (Application version) + +=== REPLACE INTO === + Initial REPLACE (insert): + app.name = CUBRID Cookbook v2 (Updated application name) + app.version = 1.0.0 (Application version) + db.host = localhost (Database host) + + After REPLACE (replaced): + app.name = CUBRID Cookbook v2 (Updated application name) + app.version = 1.0.0 (Application version) + db.host = production.db.local (Production database host) + +=== MERGE Statement === + Initial counters: + page_views: 100 + api_calls: 50 + + After MERGE (increment existing, insert new): + api_calls: 55 + errors: 1 + page_views: 110 + +โœ“ Cleaned up diff --git a/fundamentals/orm-basics/expected/06_reflection.expected b/fundamentals/orm-basics/expected/06_reflection.expected new file mode 100644 index 0000000..32f8bb2 --- /dev/null +++ b/fundamentals/orm-basics/expected/06_reflection.expected @@ -0,0 +1,47 @@ +=== Reflection โ€” Setup === + โœ“ Created tables with FK and index + +=== List Tables === + Cookbook tables (2): + โ€ข cookbook_ref_articles + โ€ข cookbook_ref_authors + +=== Reflect Columns === + + cookbook_ref_authors: + Column Type Nullable Default + ------ ---- -------- ------- + id INTEGER No None + name VARCHAR(100) No None + country VARCHAR(50) Yes None + + cookbook_ref_articles: + Column Type Nullable Default + ------ ---- -------- ------- + id INTEGER No None + title VARCHAR(200) No None + author_id INTEGER Yes None + views INTEGER Yes None + rating DOUBLE Yes None + +=== Reflect Constraints === + cookbook_ref_authors PK: ['id'] + cookbook_ref_articles PK: ['id'] + + Foreign keys on cookbook_ref_articles: + +=== Reflect Indexes === + Indexes on cookbook_ref_articles: + fk_cookbook_ref_articles_author_id columns=['author_id'] + idx_articles_views columns=['views'] + +=== Autoload Table === + Reflected 'cookbook_ref_authors' with 3 columns: + โ€ข id: INTEGER + โ€ข name: VARCHAR(100) + โ€ข country: VARCHAR(50) + + Queried 1 row(s) from reflected table: + id=1, name=Test Author, country=US + +โœ“ Cleaned up diff --git a/fundamentals/prepared-statements/expected/04_prepared.expected b/fundamentals/prepared-statements/expected/04_prepared.expected new file mode 100644 index 0000000..4a65eaf --- /dev/null +++ b/fundamentals/prepared-statements/expected/04_prepared.expected @@ -0,0 +1,25 @@ +=== Parameterized Queries === + โœ“ Inserted 3 products with parameterized queries + Electronics > $50: [('Laptop', 999.99)] + +=== SQL Injection Safety === + Malicious input returned 0 rows (table is safe!) + โœ“ Table still has 3 rows โ€” SQL injection prevented + +=== Batch Insert (executemany) === + โœ“ Inserted 8 products in {{TIME}}ms + Total products: 11 + +=== Batch Update === + โœ“ Applied 10% discount to 3 products + Laptop โ†’ $899.99 + Monitor โ†’ $314.99 + Standing Desk โ†’ $539.10 + +=== Aggregate Queries === + Category Count Avg Price Stock + -------- ----- --------- ----- + Electronics 7 $ 229.28 915 + Furniture 4 $ 296.77 135 + +โœ“ Cleaned up diff --git a/fundamentals/transactions/expected/03_transactions.expected b/fundamentals/transactions/expected/03_transactions.expected new file mode 100644 index 0000000..389ac65 --- /dev/null +++ b/fundamentals/transactions/expected/03_transactions.expected @@ -0,0 +1,25 @@ + +=== Commit Example === + Balances (before): Alice=$1000.00, Bob=$500.00 + โœ“ Transferred $200.00 from Alice to Bob + Balances (after commit): Alice=$800.00, Bob=$700.00 + +=== Rollback Example === + Balances (before): Alice=$800.00, Bob=$700.00 + Made Alice's balance = 0 (not committed) + โœ“ Rolled back โ€” Alice's balance restored + Balances (after rollback): Alice=$800.00, Bob=$700.00 + +=== Autocommit Example === + โœ“ Updated Alice +$50 (auto-committed immediately) + Balances (autocommit): Alice=$850.00, Bob=$700.00 + +=== Savepoint Example === + Balances (before): Alice=$850.00, Bob=$700.00 + Step 1: Alice +$100 + โœ“ Created SAVEPOINT 'after_alice_bonus' + Step 2: Bob +$999 (will be rolled back) + โœ“ Rolled back to savepoint โ€” Bob's $999 undone, Alice's $100 kept + Balances (after savepoint rollback + commit): Alice=$950.00, Bob=$700.00 + +โœ“ Cleaned up diff --git a/scripts/normalize_output.sh b/scripts/normalize_output.sh new file mode 100755 index 0000000..b4b2e96 --- /dev/null +++ b/scripts/normalize_output.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Normalize dynamic values in example output for reproducible comparison. +# Usage: normalize_output.sh < actual_output > normalized_output +# Compatible with both GNU sed and BSD sed (macOS). + +# Filter out SQLAlchemy cache warnings (stderr leaks) +grep -v '^/.*SAWarning' | \ +grep -v '^\s*session\.execute' | \ +grep -v '^\s*conn\.execute' | \ +grep -v 'sqlalche\.me' | \ +grep -v 'inherit_cache' | \ +grep -v 'SQL compilation caching' | \ +grep -v 'performance implications' | \ +grep -v 'set the .inherit_cache' | \ +grep -v 'this attribute may be set' | \ +sed -E \ + -e 's/CUBRID version: [0-9.]+/CUBRID version: {{VERSION}}/g' \ + -e 's/DBA@[a-zA-Z0-9_.-]+/DBA@{{HOSTNAME}}/g' \ + -e 's/[0-9]{4}-[0-9]{2}-[0-9]{2}/{{DATE}}/g' \ + -e 's/in [0-9]+\.[0-9]+ms/in {{TIME}}ms/g' \ + -e 's/in [0-9]+\.[0-9]+s]/in {{TIME}}s]/g' \ + -e 's/CLASS_OID: [0-9|]+/CLASS_OID: {{OID}}/g' \ + -e 's/B[+]tree: [0-9|]+/B+tree: {{BTREE}}/g' \ + -e 's/OID: [0-9|]+/OID: {{OID}}/g' \ + -e 's/In line [0-9]+, column [0-9]+/In line {{LINE}}, column {{COL}}/g' \ + -e 's/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]+/{{TIMESTAMP}}/g' \ + -e 's/[{][{]DATE[}][}] [0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]+/{{TIMESTAMP}}/g' \ + -e 's/[{][{]DATE[}][}]T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+/{{DATETIME}}/g' \ + -e 's/\[generated in [0-9.]+s]/[generated in {{TIME}}s]/g' From 2e042474b246a3717667b64cd7d4239f07493ed0 Mon Sep 17 00:00:00 2001 From: Yeongseon Choe Date: Sat, 4 Apr 2026 13:15:06 +0000 Subject: [PATCH 2/3] feat: extend make verify to migration, quickstart, and batch-etl examples Add expected output files for 11 additional examples beyond the initial 12 fundamentals, bringing total coverage to 23/23 runnable examples passing make verify. Fixes: - migration/03_crud_orm.py: use :.2f in __repr__ for CUBRID float precision - migration/05_batch_operations.py: reorder main block so UPDATE/DELETE operate on correct dataset instead of post-reset empty table - quickstart/5min-sqlalchemy/app.py: add drop_all before create_all for deterministic auto_increment IDs - templates/batch-etl/seed_data.py: rename to 00_seed_data.py for correct sort-order execution in find|sort pipeline - scripts/normalize_output.sh: add bare Version: pattern normalization Ref: #7 --- migration/java-to-python/03_crud_orm.py | 2 +- .../java-to-python/05_batch_operations.py | 14 ++-- .../expected/01_connection.expected | 19 +++++ .../expected/02_crud_dbapi.expected | 43 ++++++++++ .../expected/03_crud_orm.expected | 36 ++++++++ .../expected/04_transaction.expected | 31 +++++++ .../expected/05_batch_operations.expected | 17 ++++ quickstart/5min-sqlalchemy/app.py | 2 + .../5min-sqlalchemy/expected/app.expected | 7 ++ scripts/normalize_output.sh | 1 + .../{seed_data.py => 00_seed_data.py} | 0 templates/batch-etl/01_read_data.py | 2 +- templates/batch-etl/02_analysis.py | 2 +- templates/batch-etl/03_write_data.py | 2 +- templates/batch-etl/04_etl_pipeline.py | 2 +- templates/batch-etl/README.md | 6 +- .../batch-etl/expected/00_seed_data.expected | 21 +++++ .../batch-etl/expected/01_read_data.expected | 46 ++++++++++ .../batch-etl/expected/02_analysis.expected | 84 +++++++++++++++++++ .../batch-etl/expected/03_write_data.expected | 37 ++++++++ .../expected/04_etl_pipeline.expected | 40 +++++++++ 21 files changed, 400 insertions(+), 14 deletions(-) create mode 100644 migration/java-to-python/expected/01_connection.expected create mode 100644 migration/java-to-python/expected/02_crud_dbapi.expected create mode 100644 migration/java-to-python/expected/03_crud_orm.expected create mode 100644 migration/java-to-python/expected/04_transaction.expected create mode 100644 migration/java-to-python/expected/05_batch_operations.expected create mode 100644 quickstart/5min-sqlalchemy/expected/app.expected rename templates/batch-etl/{seed_data.py => 00_seed_data.py} (100%) create mode 100644 templates/batch-etl/expected/00_seed_data.expected create mode 100644 templates/batch-etl/expected/01_read_data.expected create mode 100644 templates/batch-etl/expected/02_analysis.expected create mode 100644 templates/batch-etl/expected/03_write_data.expected create mode 100644 templates/batch-etl/expected/04_etl_pipeline.expected diff --git a/migration/java-to-python/03_crud_orm.py b/migration/java-to-python/03_crud_orm.py index 7ba873b..8a8c28f 100644 --- a/migration/java-to-python/03_crud_orm.py +++ b/migration/java-to-python/03_crud_orm.py @@ -73,7 +73,7 @@ class Product(Base): price: Mapped[float] = mapped_column(default=0.0) def __repr__(self) -> str: - return f"Product(id={self.id}, val={self.val!r}, cnt={self.cnt}, price={self.price})" + return f"Product(id={self.id}, val={self.val!r}, cnt={self.cnt}, price={self.price:.2f})" def create_tables(engine) -> None: diff --git a/migration/java-to-python/05_batch_operations.py b/migration/java-to-python/05_batch_operations.py index 025fd8b..6f46441 100644 --- a/migration/java-to-python/05_batch_operations.py +++ b/migration/java-to-python/05_batch_operations.py @@ -256,12 +256,6 @@ def cleanup(conn: pycubrid.Connection) -> None: print("=== Batch INSERT ===") batch_insert_executemany(conn) - setup(conn) - batch_insert_chunked(conn) - - setup(conn) - batch_insert_single_commit(conn) - print("\n=== Batch UPDATE ===") batch_update(conn) @@ -269,6 +263,14 @@ def cleanup(conn: pycubrid.Connection) -> None: batch_delete(conn) verify_counts(conn) + + print("\n=== Batch INSERT (chunked) ===") + setup(conn) + batch_insert_chunked(conn) + + print("\n=== Batch INSERT (single commit) ===") + setup(conn) + batch_insert_single_commit(conn) finally: cleanup(conn) conn.close() diff --git a/migration/java-to-python/expected/01_connection.expected b/migration/java-to-python/expected/01_connection.expected new file mode 100644 index 0000000..0c7b3e0 --- /dev/null +++ b/migration/java-to-python/expected/01_connection.expected @@ -0,0 +1,19 @@ +=== Basic Connection === +1 + 1 = 2 + +=== Context Manager === +CUBRID version: {{VERSION}} + +=== Connection Metadata === +Version: {{VERSION}} +Database: testdb +User: DBA@{{HOSTNAME}} + +=== Auto-commit Control === +Default autocommit: False +After enable: True +After disable: False + +=== Multiple Connections === +Connection 1: conn1 +Connection 2: conn2 diff --git a/migration/java-to-python/expected/02_crud_dbapi.expected b/migration/java-to-python/expected/02_crud_dbapi.expected new file mode 100644 index 0000000..ee924fb --- /dev/null +++ b/migration/java-to-python/expected/02_crud_dbapi.expected @@ -0,0 +1,43 @@ +Created table 'cookbook_items' +Inserted 1 row +Inserted 4 rows + +All items (5 rows): + ID Value Count Price + 1 Widget A 10 $ 29.99 + 2 Widget B 5 $ 19.99 + 3 Gadget C 20 $ 49.99 + 4 Part D 100 $ 2.50 + 5 Tool E 8 $ 34.99 + +Items over $20 (3 rows): + Gadget C $49.99 + Tool E $34.99 + Widget A $29.99 + +First item: Widget A (count=10) +Second item: Widget B (count=5) + +Updated Widget A price (rows affected: 1) +Restocked cheap items (rows affected: 1) + +All items (5 rows): + ID Value Count Price + 1 Widget A 10 $ 24.99 + 2 Widget B 5 $ 19.99 + 3 Gadget C 20 $ 49.99 + 4 Part D 110 $ 2.50 + 5 Tool E 8 $ 34.99 + +Deleted Tool E (rows affected: 1) + +All items (4 rows): + ID Value Count Price + 1 Widget A 10 $ 24.99 + 2 Widget B 5 $ 19.99 + 3 Gadget C 20 $ 49.99 + 4 Part D 110 $ 2.50 + +Null handling: val='NullTest', cnt=None, price=None + +Cleaned up table 'cookbook_items' diff --git a/migration/java-to-python/expected/03_crud_orm.expected b/migration/java-to-python/expected/03_crud_orm.expected new file mode 100644 index 0000000..b5d35a6 --- /dev/null +++ b/migration/java-to-python/expected/03_crud_orm.expected @@ -0,0 +1,36 @@ +Created table 'cookbook_products' +Inserted 5 products + Product(id=1, val='Widget A', cnt=10, price=29.99) + Product(id=2, val='Widget B', cnt=5, price=19.99) + Product(id=3, val='Gadget C', cnt=20, price=49.99) + Product(id=4, val='Part D', cnt=100, price=2.50) + Product(id=5, val='Tool E', cnt=8, price=34.99) + +All products (5): + Widget A cnt= 10 $29.99 + Widget B cnt= 5 $19.99 + Gadget C cnt= 20 $49.99 + Part D cnt=100 $2.50 + Tool E cnt= 8 $34.99 + +Products over $20 (3): + Gadget C $49.99 + Tool E $34.99 + Widget A $29.99 + +Aggregation: 5 products, avg $27.49, total count 143 + +Updated Widget A: $29.99 -> $24.99 +Restocked 1 cheap products (+50 each) + +All products (5): + Widget A cnt= 10 $24.99 + Widget B cnt= 5 $19.99 + Gadget C cnt= 20 $49.99 + Part D cnt=150 $2.50 + Tool E cnt= 8 $34.99 + +Deleted 'Tool E' +Remaining products: 4 + +Cleaned up diff --git a/migration/java-to-python/expected/04_transaction.expected b/migration/java-to-python/expected/04_transaction.expected new file mode 100644 index 0000000..5746a25 --- /dev/null +++ b/migration/java-to-python/expected/04_transaction.expected @@ -0,0 +1,31 @@ +Setup: 3 accounts created + +=== Commit === + Balances [before]: Alice=$1000.00, Bob=$500.00, Charlie=$750.00 + Transferred $200 Alice -> Bob + Balances [after commit]: Alice=$800.00, Bob=$700.00, Charlie=$750.00 + +=== Rollback === + Set Alice=0 (not committed) + Rolled back + Balances [after rollback]: Alice=$800.00, Bob=$700.00, Charlie=$750.00 + +=== Safe Transfer === + Balances [before transfers]: Alice=$800.00, Bob=$700.00, Charlie=$750.00 + Transfer Alice->Charlie $100: success + Transfer Bob->Alice $9999: failed (insufficient) + Balances [after transfers]: Alice=$700.00, Bob=$700.00, Charlie=$850.00 + +=== Savepoints === + Balances [before]: Alice=$700.00, Bob=$700.00, Charlie=$850.00 + Step 1: Alice +$100 + Step 2: Bob +$999 (will undo) + Rolled back to savepoint (Bob's $999 undone) + Balances [after savepoint commit]: Alice=$800.00, Bob=$700.00, Charlie=$850.00 + +=== Auto-commit Mode === + Default autocommit: False + Charlie +$1 (auto-committed, no explicit commit needed) + Balances [autocommit]: Alice=$800.00, Bob=$700.00, Charlie=$851.00 + +Cleaned up diff --git a/migration/java-to-python/expected/05_batch_operations.expected b/migration/java-to-python/expected/05_batch_operations.expected new file mode 100644 index 0000000..3f05447 --- /dev/null +++ b/migration/java-to-python/expected/05_batch_operations.expected @@ -0,0 +1,17 @@ +=== Batch INSERT === +executemany: inserted 1000 rows in {{TIME}}ms + +=== Batch UPDATE === +batch update: updated 10 rows + +=== Batch DELETE === +batch delete: removed 10 rows (WHERE cnt=999) + +Total rows remaining: 990 + +=== Batch INSERT (chunked) === +chunked (500/commit): inserted 2000 rows in {{TIME}}ms + +=== Batch INSERT (single commit) === +single commit: inserted 2000 rows in {{TIME}}ms +Cleaned up diff --git a/quickstart/5min-sqlalchemy/app.py b/quickstart/5min-sqlalchemy/app.py index daf864a..7ecf665 100644 --- a/quickstart/5min-sqlalchemy/app.py +++ b/quickstart/5min-sqlalchemy/app.py @@ -110,6 +110,8 @@ def main() -> int: engine = create_engine(DATABASE_URL) try: + metadata.drop_all(engine) + Base.metadata.drop_all(engine) metadata.create_all(engine) Base.metadata.create_all(engine) run_core_crud(engine) diff --git a/quickstart/5min-sqlalchemy/expected/app.expected b/quickstart/5min-sqlalchemy/expected/app.expected new file mode 100644 index 0000000..7a5e8e9 --- /dev/null +++ b/quickstart/5min-sqlalchemy/expected/app.expected @@ -0,0 +1,7 @@ +=== SQLAlchemy Core CRUD === +Core query result: [(1, 'core-sample', 'v1')] + +=== SQLAlchemy ORM CRUD === +ORM query result: 1 orm-sample + +Done. Core and ORM CRUD completed successfully. diff --git a/scripts/normalize_output.sh b/scripts/normalize_output.sh index b4b2e96..cab1aaa 100755 --- a/scripts/normalize_output.sh +++ b/scripts/normalize_output.sh @@ -15,6 +15,7 @@ grep -v 'set the .inherit_cache' | \ grep -v 'this attribute may be set' | \ sed -E \ -e 's/CUBRID version: [0-9.]+/CUBRID version: {{VERSION}}/g' \ + -e 's/^(Version:[[:space:]]+)[0-9.]+/\1{{VERSION}}/g' \ -e 's/DBA@[a-zA-Z0-9_.-]+/DBA@{{HOSTNAME}}/g' \ -e 's/[0-9]{4}-[0-9]{2}-[0-9]{2}/{{DATE}}/g' \ -e 's/in [0-9]+\.[0-9]+ms/in {{TIME}}ms/g' \ diff --git a/templates/batch-etl/seed_data.py b/templates/batch-etl/00_seed_data.py similarity index 100% rename from templates/batch-etl/seed_data.py rename to templates/batch-etl/00_seed_data.py diff --git a/templates/batch-etl/01_read_data.py b/templates/batch-etl/01_read_data.py index ae4fa87..ec966f9 100644 --- a/templates/batch-etl/01_read_data.py +++ b/templates/batch-etl/01_read_data.py @@ -19,7 +19,7 @@ def ensure_sales_table_exists(engine) -> bool: if not has_table: print(f"Missing required table '{SALES_TABLE_NAME}'.") - print("Run seed_data.py first to create and populate sample data.") + print("Run 00_seed_data.py first to create and populate sample data.") return False return True diff --git a/templates/batch-etl/02_analysis.py b/templates/batch-etl/02_analysis.py index 0752a1e..8a707de 100644 --- a/templates/batch-etl/02_analysis.py +++ b/templates/batch-etl/02_analysis.py @@ -16,7 +16,7 @@ def load_sales_dataframe() -> pd.DataFrame | None: try: if not inspect(engine).has_table(SALES_TABLE_NAME): print(f"Missing required table '{SALES_TABLE_NAME}'.") - print("Run seed_data.py first to create and populate sample data.") + print("Run 00_seed_data.py first to create and populate sample data.") return None df = pd.read_sql(SALES_TABLE_NAME, engine) diff --git a/templates/batch-etl/03_write_data.py b/templates/batch-etl/03_write_data.py index 3052a92..248fb3d 100644 --- a/templates/batch-etl/03_write_data.py +++ b/templates/batch-etl/03_write_data.py @@ -48,7 +48,7 @@ def main() -> int: try: if not inspect(engine).has_table(SOURCE_TABLE): print(f"Missing required source table '{SOURCE_TABLE}'.") - print("Run seed_data.py first to create and populate sample data.") + print("Run 00_seed_data.py first to create and populate sample data.") return 1 source_df = pd.read_sql_query( diff --git a/templates/batch-etl/04_etl_pipeline.py b/templates/batch-etl/04_etl_pipeline.py index f7025cc..1b7e35b 100644 --- a/templates/batch-etl/04_etl_pipeline.py +++ b/templates/batch-etl/04_etl_pipeline.py @@ -25,7 +25,7 @@ def configure_logging() -> None: def extract(engine) -> pd.DataFrame | None: if not inspect(engine).has_table(SOURCE_TABLE): logging.error("Missing required source table '%s'.", SOURCE_TABLE) - logging.error("Run seed_data.py first to create and populate sample data.") + logging.error("Run 00_seed_data.py first to create and populate sample data.") return None df = pd.read_sql(SOURCE_TABLE, engine) diff --git a/templates/batch-etl/README.md b/templates/batch-etl/README.md index d5b67eb..4d8a651 100644 --- a/templates/batch-etl/README.md +++ b/templates/batch-etl/README.md @@ -18,7 +18,7 @@ The template uses `cookbook_` table names and includes defensive checks for miss 2. Start CUBRID (for example from a repository-level compose file), then run scripts in order: ```bash - python seed_data.py + python 00_seed_data.py python 01_read_data.py python 02_analysis.py python 03_write_data.py @@ -30,7 +30,7 @@ The template uses `cookbook_` table names and includes defensive checks for miss ## Project Structure ```text -seed_data.py # Creates and seeds cookbook_sales with realistic sample rows +00_seed_data.py # Creates and seeds cookbook_sales with realistic sample rows 01_read_data.py # Demonstrates pd.read_sql_query, pd.read_sql, pd.read_sql_table 02_analysis.py # Performs descriptive stats, groupby, pivot, and date-based analysis 03_write_data.py # Demonstrates to_sql replace/append/chunked writes @@ -55,7 +55,7 @@ If your environment differs, update `DATABASE_URL` values consistently across al ## What Each Script Produces -- `seed_data.py`: recreates and seeds `cookbook_sales`. +- `00_seed_data.py`: recreates and seeds `cookbook_sales`. - `01_read_data.py`: prints SQL read examples and revenue-by-region summary. - `02_analysis.py`: prints dataset overview and analytics summaries. - `03_write_data.py`: writes demo outputs into `cookbook_sales_write_demo`. diff --git a/templates/batch-etl/expected/00_seed_data.expected b/templates/batch-etl/expected/00_seed_data.expected new file mode 100644 index 0000000..63af3cd --- /dev/null +++ b/templates/batch-etl/expected/00_seed_data.expected @@ -0,0 +1,21 @@ +Seeded 180 rows into table 'cookbook_sales'. + +Preview (first 10 rows): + sale_id sale_date product category quantity unit_price region sales_channel is_promo revenue + 1 {{DATE}} Noise-Cancel Headphones Electronics 8 225.67 South Online 0 1805.36 + 2 {{DATE}} Noise-Cancel Headphones Electronics 1 260.85 North Online 0 260.85 + 3 {{DATE}} Notebook Pro 14 Electronics 18 1351.35 West Online 1 24324.30 + 4 {{DATE}} Water Bottle Accessories 6 26.46 West Retail 1 158.76 + 5 {{DATE}} Office Chair Furniture 4 180.27 North Retail 0 721.08 + 6 {{DATE}} Backpack Accessories 9 87.84 North Distributor 1 790.56 + 7 {{DATE}} Noise-Cancel Headphones Electronics 3 284.68 Central Retail 1 854.04 + 8 {{DATE}} Standing Desk Furniture 8 536.97 East Online 0 4295.76 + 9 {{DATE}} Noise-Cancel Headphones Electronics 12 247.76 South Retail 1 2973.12 + 10 {{DATE}} Standing Desk Furniture 3 532.68 Central Distributor 0 1598.04 + +Sales by category: + category total_qty total_revenue +Electronics 584 381264.13 + Furniture 349 139726.04 +Accessories 349 17912.90 + Grocery 450 11868.69 diff --git a/templates/batch-etl/expected/01_read_data.expected b/templates/batch-etl/expected/01_read_data.expected new file mode 100644 index 0000000..8e8c01c --- /dev/null +++ b/templates/batch-etl/expected/01_read_data.expected @@ -0,0 +1,46 @@ +=== pd.read_sql_query() with raw SQL === + sale_date product category quantity unit_price region revenue +{{DATE}} Noise-Cancel Headphones Electronics 13 232.72 South 3025.36 +{{DATE}} Water Bottle Accessories 4 23.44 South 93.76 +{{DATE}} Noise-Cancel Headphones Electronics 6 281.77 West 1690.62 +{{DATE}} Office Chair Furniture 6 213.83 South 1282.98 +{{DATE}} Espresso Beans Grocery 5 18.94 North 94.70 +{{DATE}} Water Bottle Accessories 15 26.89 Central 403.35 +{{DATE}} Protein Bars Grocery 9 32.40 Central 291.60 +{{DATE}} Notebook Pro 14 Electronics 7 1414.09 North 9898.63 +{{DATE}} Office Chair Furniture 6 200.21 West 1201.26 +{{DATE}} Backpack Accessories 6 81.58 East 489.48 +{{DATE}} Noise-Cancel Headphones Electronics 8 225.67 South 1805.36 +{{DATE}} Noise-Cancel Headphones Electronics 16 256.10 South 4097.60 +Rows returned: 135 + +=== pd.read_sql() with table name === + sale_id sale_date product category quantity unit_price region sales_channel is_promo + 1 {{DATE}} Noise-Cancel Headphones Electronics 8 225.67 South Online 0 + 2 {{DATE}} Noise-Cancel Headphones Electronics 1 260.85 North Online 0 + 3 {{DATE}} Notebook Pro 14 Electronics 18 1351.35 West Online 1 + 4 {{DATE}} Water Bottle Accessories 6 26.46 West Retail 1 + 5 {{DATE}} Office Chair Furniture 4 180.27 North Retail 0 + 6 {{DATE}} Backpack Accessories 9 87.84 North Distributor 1 + 7 {{DATE}} Noise-Cancel Headphones Electronics 3 284.68 Central Retail 1 + 8 {{DATE}} Standing Desk Furniture 8 536.97 East Online 0 +Rows in cookbook_sales: 180 + +=== pd.read_sql_table() selecting columns === + sale_date product region quantity unit_price +{{DATE}} Noise-Cancel Headphones South 8 225.67 +{{DATE}} Noise-Cancel Headphones North 1 260.85 +{{DATE}} Notebook Pro 14 West 18 1351.35 +{{DATE}} Water Bottle West 6 26.46 +{{DATE}} Office Chair North 4 180.27 +{{DATE}} Backpack North 9 87.84 +{{DATE}} Noise-Cancel Headphones Central 3 284.68 +{{DATE}} Standing Desk East 8 536.97 + +=== Quick metric: Revenue by region (from read_sql_table data) === + region revenue +Central 132711.58 + East 120533.60 + West 108417.01 + North 103332.82 + South 85776.75 diff --git a/templates/batch-etl/expected/02_analysis.expected b/templates/batch-etl/expected/02_analysis.expected new file mode 100644 index 0000000..9a1a6a4 --- /dev/null +++ b/templates/batch-etl/expected/02_analysis.expected @@ -0,0 +1,84 @@ +=== Dataset Overview === + sale_id sale_date product category quantity unit_price region sales_channel is_promo revenue + 1 {{DATE}} Noise-Cancel Headphones Electronics 8 225.67 South Online 0 1805.36 + 2 {{DATE}} Noise-Cancel Headphones Electronics 1 260.85 North Online 0 260.85 + 3 {{DATE}} Notebook Pro 14 Electronics 18 1351.35 West Online 1 24324.30 + 4 {{DATE}} Water Bottle Accessories 6 26.46 West Retail 1 158.76 + 5 {{DATE}} Office Chair Furniture 4 180.27 North Retail 0 721.08 + 6 {{DATE}} Backpack Accessories 9 87.84 North Distributor 1 790.56 + 7 {{DATE}} Noise-Cancel Headphones Electronics 3 284.68 Central Retail 1 854.04 + 8 {{DATE}} Standing Desk Furniture 8 536.97 East Online 0 4295.76 + 9 {{DATE}} Noise-Cancel Headphones Electronics 12 247.76 South Retail 1 2973.12 + 10 {{DATE}} Standing Desk Furniture 3 532.68 Central Distributor 0 1598.04 + +=== Statistical Summary (describe) === + quantity unit_price revenue +count 180.000000 180.000000 180.000000 +mean 9.622222 308.693056 3059.843111 +std 5.351505 407.205020 4790.338023 +min 1.000000 16.900000 19.390000 +25% 5.000000 30.815000 285.232500 +50% 9.000000 179.710000 827.305000 +75% 14.000000 329.727500 4097.727500 +max 20.000000 1486.710000 24460.200000 + +=== Product Frequency (value_counts) === +product +Noise-Cancel Headphones 32 +Protein Bars 25 +Standing Desk 23 +Notebook Pro 14 22 +Espresso Beans 22 +Backpack 21 +Water Bottle 18 +Office Chair 17 + +=== GroupBy: Category and Region === + category region total_quantity total_revenue orders +Accessories Central 113 6324.83 10 +Accessories East 42 1926.82 7 +Accessories North 66 4353.03 6 +Accessories South 60 2500.55 8 +Accessories West 68 2807.67 8 +Electronics Central 140 95816.03 12 +Electronics East 120 70720.65 12 +Electronics North 96 70706.45 10 +Electronics South 144 71072.43 12 +Electronics West 84 72948.57 8 + Furniture Central 69 27867.16 10 + Furniture East 94 45182.13 11 + Furniture North 74 25701.27 8 + Furniture South 34 10888.98 4 + Furniture West 78 30086.50 7 + Grocery Central 104 2703.56 13 + Grocery East 107 2704.00 10 + Grocery North 96 2572.07 11 + Grocery South 57 1314.79 5 + Grocery West 86 2574.27 8 + +=== Pivot Table: Revenue by Category x Region === +region Central East North South West +category +Accessories 6324.83 1926.82 4353.03 2500.55 2807.67 +Electronics 95816.03 70720.65 70706.45 71072.43 72948.57 +Furniture 27867.16 45182.13 25701.27 10888.98 30086.50 +Grocery 2703.56 2704.00 2572.07 1314.79 2574.27 + +=== Date-based Analysis: Monthly Sales === +year_month total_quantity total_revenue order_count + 2025-01 261 56403.64 26 + 2025-02 175 64953.22 19 + 2025-03 318 107948.31 33 + 2025-04 308 101680.54 31 + 2025-05 364 99691.06 38 + 2025-06 306 120094.99 33 + +=== Date-based Analysis: Day of Week Revenue === + day_name total_revenue orders + Friday 54996.92 17 + Monday 54350.91 15 + Saturday 55971.87 26 + Sunday 103963.56 31 + Thursday 136547.74 38 + Tuesday 71479.41 25 +Wednesday 73461.35 28 diff --git a/templates/batch-etl/expected/03_write_data.expected b/templates/batch-etl/expected/03_write_data.expected new file mode 100644 index 0000000..c835c63 --- /dev/null +++ b/templates/batch-etl/expected/03_write_data.expected @@ -0,0 +1,37 @@ +=== Initial DataFrame to write (replace) === + demo_id sale_date product category region quantity + 1 {{DATE}} Noise-Cancel Headphones Electronics South 8 + 2 {{DATE}} Noise-Cancel Headphones Electronics North 1 + 3 {{DATE}} Notebook Pro 14 Electronics West 18 + 4 {{DATE}} Water Bottle Accessories West 6 + 5 {{DATE}} Office Chair Furniture North 4 + 6 {{DATE}} Backpack Accessories North 9 + 7 {{DATE}} Noise-Cancel Headphones Electronics Central 3 + 8 {{DATE}} Standing Desk Furniture East 8 + 9 {{DATE}} Noise-Cancel Headphones Electronics South 12 + 10 {{DATE}} Standing Desk Furniture Central 3 +Wrote 25 rows to 'cookbook_sales_write_demo' with if_exists='replace'. +Appended 10 rows with if_exists='append'. +Appended 50 rows using chunked to_sql(chunksize=10). + +=== Final row count in target table === + row_count + 85 + +=== Preview of target data === + demo_id sale_date product category region quantity + 1 {{DATE}} Noise-Cancel Headphones Electronics South 8 + 2 {{DATE}} Noise-Cancel Headphones Electronics North 1 + 3 {{DATE}} Notebook Pro 14 Electronics West 18 + 4 {{DATE}} Water Bottle Accessories West 6 + 5 {{DATE}} Office Chair Furniture North 4 + 6 {{DATE}} Backpack Accessories North 9 + 7 {{DATE}} Noise-Cancel Headphones Electronics Central 3 + 8 {{DATE}} Standing Desk Furniture East 8 + 9 {{DATE}} Noise-Cancel Headphones Electronics South 12 + 10 {{DATE}} Standing Desk Furniture Central 3 + 11 {{DATE}} Standing Desk Furniture Central 9 + 12 {{DATE}} Notebook Pro 14 Electronics West 11 + 13 {{DATE}} Standing Desk Furniture South 11 + 14 {{DATE}} Espresso Beans Grocery South 15 + 15 {{DATE}} Standing Desk Furniture Central 9 diff --git a/templates/batch-etl/expected/04_etl_pipeline.expected b/templates/batch-etl/expected/04_etl_pipeline.expected new file mode 100644 index 0000000..8da2dc3 --- /dev/null +++ b/templates/batch-etl/expected/04_etl_pipeline.expected @@ -0,0 +1,40 @@ +{{TIMESTAMP}} | INFO | Extracted 180 rows from cookbook_sales. +{{TIMESTAMP}} | INFO | Transformed data: 180 cleaned rows, 93 summary rows. +{{TIMESTAMP}} | INFO | Loaded cleaned table 'cookbook_sales_cleaned' and summary table 'cookbook_sales_summary'. +=== ETL Completed: Cleaned Data Preview === + sale_id sale_date product category quantity unit_price region sales_channel is_promo revenue year_month + 1 {{DATE}} Noise-Cancel Headphones Electronics 8 225.67 South Online 0 1805.36 2025-03 + 2 {{DATE}} Noise-Cancel Headphones Electronics 1 260.85 North Online 0 260.85 2025-01 + 3 {{DATE}} Notebook Pro 14 Electronics 18 1351.35 West Online 1 24324.30 2025-06 + 4 {{DATE}} Water Bottle Accessories 6 26.46 West Retail 1 158.76 2025-01 + 5 {{DATE}} Office Chair Furniture 4 180.27 North Retail 0 721.08 2025-03 + 6 {{DATE}} Backpack Accessories 9 87.84 North Distributor 1 790.56 2025-06 + 7 {{DATE}} Noise-Cancel Headphones Electronics 3 284.68 Central Retail 1 854.04 2025-04 + 8 {{DATE}} Standing Desk Furniture 8 536.97 East Online 0 4295.76 2025-01 + 9 {{DATE}} Noise-Cancel Headphones Electronics 12 247.76 South Retail 1 2973.12 2025-04 + 10 {{DATE}} Standing Desk Furniture 3 532.68 Central Distributor 0 1598.04 2025-06 + 11 {{DATE}} Standing Desk Furniture 9 469.51 Central Online 1 4225.59 2025-04 + 12 {{DATE}} Notebook Pro 14 Electronics 11 1243.53 West Retail 0 13678.83 2025-01 + +=== ETL Completed: Summary Preview === +year_month category region total_orders total_quantity total_revenue avg_unit_price avg_order_quantity + 2025-01 Accessories Central 1 13 989.04 76.080000 13.0 + 2025-01 Accessories East 1 19 500.27 26.330000 19.0 + 2025-01 Accessories South 2 10 833.24 58.020000 5.0 + 2025-01 Accessories West 2 7 183.10 25.400000 3.5 + 2025-01 Electronics North 3 15 11292.93 670.046667 5.0 + 2025-01 Electronics South 1 16 3966.40 247.900000 16.0 + 2025-01 Electronics West 2 21 16006.73 738.160000 10.5 + 2025-01 Furniture East 3 24 9335.06 416.640000 8.0 + 2025-01 Furniture North 1 19 4098.11 215.690000 19.0 + 2025-01 Furniture West 2 26 6904.48 367.555000 13.0 + 2025-01 Grocery Central 1 9 156.42 17.380000 9.0 + 2025-01 Grocery East 5 51 1327.23 26.002000 10.2 + 2025-01 Grocery West 2 31 810.63 26.385000 15.5 + 2025-02 Accessories Central 1 20 524.00 26.200000 20.0 + 2025-02 Accessories North 2 13 731.79 50.040000 6.5 + 2025-02 Accessories West 1 17 1282.31 75.430000 17.0 + 2025-02 Electronics Central 2 30 25941.74 825.280000 15.0 + 2025-02 Electronics East 1 7 1981.63 283.090000 7.0 + 2025-02 Electronics North 1 9 12591.18 1399.020000 9.0 + 2025-02 Electronics West 1 4 5193.40 1298.350000 4.0 From 8b6aa0e9206a806278c9468b4125de20a2cbae40 Mon Sep 17 00:00:00 2001 From: Yeongseon Choe Date: Sat, 4 Apr 2026 14:19:08 +0000 Subject: [PATCH 3/3] ci: update workflows for restructured Python-only layout - ci.yml: remove Node/Go lint jobs, run ruff on repo root instead of obsolete python/ directory - smoke-test.yml: replace old multi-language matrix with single make verify job against CUBRID service - Format 4 Python files to pass ruff format --check --- .github/workflows/ci.yml | 41 +-- .github/workflows/smoke-test.yml | 294 +----------------- migration/java-to-python/03_crud_orm.py | 10 +- .../java-to-python/05_batch_operations.py | 28 +- quickstart/5min-fastapi/app.py | 7 + quickstart/5min-sqlalchemy/app.py | 16 +- 6 files changed, 38 insertions(+), 358 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f637ba..3252a28 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,43 +20,6 @@ jobs: - name: Install ruff run: pip install ruff - name: Ruff check - run: ruff check python/ + run: ruff check . - name: Ruff format check - run: ruff format --check python/ - - lint-typescript: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: "22" - - name: Check TypeScript syntax - run: | - for dir in node/*/; do - if [ -f "$dir/package.json" ]; then - echo "Checking $dir..." - cd "$dir" - npm install --ignore-scripts 2>/dev/null || true - npx tsc --noEmit 2>/dev/null || echo "No tsconfig in $dir" - cd - - fi - done - - lint-go: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: "1.22" - - name: Check Go syntax - run: | - for dir in go/*/; do - if [ -f "$dir/go.mod" ]; then - echo "Checking $dir..." - cd "$dir" - go vet ./... 2>/dev/null || echo "go vet issues in $dir" - cd - - fi - done + run: ruff format --check . diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml index a47bfbe..e34288c 100644 --- a/.github/workflows/smoke-test.yml +++ b/.github/workflows/smoke-test.yml @@ -12,20 +12,8 @@ concurrency: cancel-in-progress: true jobs: - python-examples: + verify: runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - category: - - pycubrid - - sqlalchemy - - fastapi - - flask - - django - - pandas - - celery - - streamlit services: cubrid: image: cubrid/cubrid:11.2 @@ -48,8 +36,8 @@ jobs: with: python-version: "3.12" - - name: Install pycubrid for readiness check - run: pip install pycubrid + - name: Install dependencies + run: pip install pycubrid sqlalchemy sqlalchemy-cubrid - name: Wait for CUBRID readiness run: | @@ -74,277 +62,5 @@ jobs: sleep 5 done - - name: Install requirements for ${{ matrix.category }} - run: | - req_file="python/${{ matrix.category }}/requirements.txt" - if [ -f "$req_file" ]; then - pip install -r "$req_file" - fi - - - name: Run ${{ matrix.category }} examples - id: run_category - continue-on-error: true - run: | - set +e - passed=0 - failed=0 - - run_example() { - file="$1" - echo "Running $file" - if python "$file"; then - passed=$((passed + 1)) - else - failed=$((failed + 1)) - fi - } - - case "${{ matrix.category }}" in - pycubrid|sqlalchemy) - for file in python/${{ matrix.category }}/*.py; do - run_example "$file" - done - ;; - pandas) - run_example "python/pandas/seed_data.py" - for file in python/pandas/[0-9][0-9]_*.py; do - run_example "$file" - done - ;; - fastapi) - echo "Import check: app.main" - if PYTHONPATH=python/fastapi python -c "import app.main"; then - passed=$((passed + 1)) - else - failed=$((failed + 1)) - fi - ;; - flask) - echo "Import check: run" - if PYTHONPATH=python/flask python -c "import run"; then - passed=$((passed + 1)) - else - failed=$((failed + 1)) - fi - ;; - django) - echo "Import check: manage + settings" - if PYTHONPATH=python/django python -c "import manage; import cubrid_project.settings"; then - passed=$((passed + 1)) - else - failed=$((failed + 1)) - fi - ;; - celery) - echo "Import check: app + run_tasks" - if PYTHONPATH=python/celery python -c "import app; import run_tasks"; then - passed=$((passed + 1)) - else - failed=$((failed + 1)) - fi - ;; - streamlit) - echo "Import check: app" - if PYTHONPATH=python/streamlit python -c "import app"; then - passed=$((passed + 1)) - else - failed=$((failed + 1)) - fi - ;; - esac - - echo "passed=$passed" >> "$GITHUB_OUTPUT" - echo "failed=$failed" >> "$GITHUB_OUTPUT" - - if [ "$failed" -gt 0 ]; then - exit 1 - fi - - - name: Print summary for ${{ matrix.category }} - if: always() - run: | - echo "Python category: ${{ matrix.category }}" - echo "Passed: ${{ steps.run_category.outputs.passed || '0' }}" - echo "Failed: ${{ steps.run_category.outputs.failed || '0' }}" - { - echo "### Python smoke summary (${{ matrix.category }})" - echo "- Passed: ${{ steps.run_category.outputs.passed || '0' }}" - echo "- Failed: ${{ steps.run_category.outputs.failed || '0' }}" - } >> "$GITHUB_STEP_SUMMARY" - if [ "${{ steps.run_category.outcome }}" != "success" ]; then - exit 1 - fi - - node-examples: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - example_set: - - cubrid - - drizzle - services: - cubrid: - image: cubrid/cubrid:11.2 - env: - CUBRID_DB: testdb - ports: - - 33000:33000 - options: >- - --health-cmd "csql -u dba testdb -c 'SELECT 1'" - --health-interval 15s - --health-timeout 10s - --health-retries 10 - --health-start-period 30s - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "22" - - - name: Ensure ESM mode in package.json - run: | - node -e " - const fs = require('fs'); - const path = 'node/${{ matrix.example_set }}/package.json'; - const pkg = JSON.parse(fs.readFileSync(path, 'utf8')); - if (pkg.type !== 'module') { - console.error(path + ' must include \"type\": \"module\"'); - process.exit(1); - } - console.log(path + ' is configured for ESM'); - " - - - name: Wait for CUBRID readiness - run: | - for i in $(seq 1 30); do - if node -e " - const net = require('net'); - const s = net.createConnection(33000, 'localhost'); - s.on('connect', () => { s.destroy(); process.exit(0); }); - s.on('error', () => process.exit(1)); - setTimeout(() => process.exit(1), 5000); - "; then break; fi - echo "Waiting... ($i/30)" - sleep 5 - done - - - name: Install dependencies - run: npm install - working-directory: node/${{ matrix.example_set }} - - - name: Run ${{ matrix.example_set }} examples - run: | - set +e - passed=0 - failed=0 - - for file in node/${{ matrix.example_set }}/*.js; do - echo "Running $file" - if node "$file"; then - passed=$((passed + 1)) - else - failed=$((failed + 1)) - fi - done - - echo "Node example set: ${{ matrix.example_set }}" - echo "Passed: $passed" - echo "Failed: $failed" - { - echo "### Node smoke summary (${{ matrix.example_set }})" - echo "- Passed: $passed" - echo "- Failed: $failed" - } >> "$GITHUB_STEP_SUMMARY" - - if [ "$failed" -gt 0 ]; then - exit 1 - fi - - go-examples: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - example_set: - - cubrid-go - - gorm - services: - cubrid: - image: cubrid/cubrid:11.2 - env: - CUBRID_DB: testdb - ports: - - 33000:33000 - options: >- - --health-cmd "csql -u dba testdb -c 'SELECT 1'" - --health-interval 15s - --health-timeout 10s - --health-retries 10 - --health-start-period 30s - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: "1.22" - - - name: Wait for CUBRID readiness - run: | - for i in $(seq 1 30); do - if python -c " - import socket - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.settimeout(5) - try: - s.connect(('localhost', 33000)) - s.close() - print('CUBRID ready') - exit(0) - except Exception: - exit(1) - "; then - break - fi - echo "Waiting... ($i/30)" - sleep 5 - done - - - name: Build ${{ matrix.example_set }} examples - run: go build ./... - working-directory: go/${{ matrix.example_set }} - - - name: Compile Go examples in ${{ matrix.example_set }} - working-directory: go/${{ matrix.example_set }} - run: | - set +e - passed=0 - failed=0 - - for file in *.go; do - [ "$file" = "*.go" ] && break - echo "Compiling $file" - if go build -o /dev/null "$file"; then - passed=$((passed + 1)) - else - failed=$((failed + 1)) - fi - done - - echo "Go example set: ${{ matrix.example_set }}" - echo "Compiled: $passed" - echo "Failed: $failed" - { - echo "### Go smoke summary (${{ matrix.example_set }})" - echo "- Compiled: $passed" - echo "- Failed: $failed" - } >> "$GITHUB_STEP_SUMMARY" - - if [ "$failed" -gt 0 ]; then - exit 1 - fi + - name: Run make verify + run: make verify diff --git a/migration/java-to-python/03_crud_orm.py b/migration/java-to-python/03_crud_orm.py index 8a8c28f..ac8d715 100644 --- a/migration/java-to-python/03_crud_orm.py +++ b/migration/java-to-python/03_crud_orm.py @@ -150,11 +150,7 @@ def query_filtered(engine) -> None: Python โ€” method chaining replaces JPQL string building: """ with Session(engine) as session: - stmt = ( - select(Product) - .where(Product.price > 20.0) - .order_by(Product.price.desc()) - ) + stmt = select(Product).where(Product.price > 20.0).order_by(Product.price.desc()) products = session.scalars(stmt).all() print(f"\nProducts over $20 ({len(products)}):") @@ -180,7 +176,9 @@ def query_aggregation(engine) -> None: func.sum(Product.cnt).label("total_cnt"), ) row = session.execute(stmt).one() - print(f"\nAggregation: {row.total} products, avg ${row.avg_price:.2f}, total count {row.total_cnt}") + print( + f"\nAggregation: {row.total} products, avg ${row.avg_price:.2f}, total count {row.total_cnt}" + ) def update_products(engine) -> None: diff --git a/migration/java-to-python/05_batch_operations.py b/migration/java-to-python/05_batch_operations.py index 6f46441..0dca47e 100644 --- a/migration/java-to-python/05_batch_operations.py +++ b/migration/java-to-python/05_batch_operations.py @@ -86,10 +86,7 @@ def batch_insert_executemany(conn: pycubrid.Connection) -> None: Python โ€” executemany handles the loop internally: """ - sensor_data = [ - (f"sensor_{i:04d}", i % 100, f"reading_{i}") - for i in range(1000) - ] + sensor_data = [(f"sensor_{i:04d}", i % 100, f"reading_{i}") for i in range(1000)] cursor = conn.cursor() @@ -101,7 +98,7 @@ def batch_insert_executemany(conn: pycubrid.Connection) -> None: conn.commit() elapsed = time.perf_counter() - t0 - print(f"executemany: inserted {len(sensor_data)} rows in {elapsed*1000:.1f}ms") + print(f"executemany: inserted {len(sensor_data)} rows in {elapsed * 1000:.1f}ms") cursor.close() @@ -124,17 +121,14 @@ def batch_insert_chunked(conn: pycubrid.Connection) -> None: Python โ€” chunk with slicing, commit per chunk: """ - rows = [ - (f"bulk_{i:05d}", i % 256, f"data_{i}") - for i in range(2000) - ] + rows = [(f"bulk_{i:05d}", i % 256, f"data_{i}") for i in range(2000)] chunk_size = 500 cursor = conn.cursor() t0 = time.perf_counter() for offset in range(0, len(rows), chunk_size): - chunk = rows[offset:offset + chunk_size] + chunk = rows[offset : offset + chunk_size] cursor.executemany( "INSERT INTO cookbook_sensors (val, cnt, file_data) VALUES (?, ?, ?)", chunk, @@ -142,7 +136,7 @@ def batch_insert_chunked(conn: pycubrid.Connection) -> None: conn.commit() elapsed = time.perf_counter() - t0 - print(f"chunked ({chunk_size}/commit): inserted {len(rows)} rows in {elapsed*1000:.1f}ms") + print(f"chunked ({chunk_size}/commit): inserted {len(rows)} rows in {elapsed * 1000:.1f}ms") cursor.close() @@ -154,10 +148,7 @@ def batch_insert_single_commit(conn: pycubrid.Connection) -> None: Trade-off: If insertion fails mid-batch, all rows roll back. """ - rows = [ - (f"fast_{i:05d}", i % 256, f"data_{i}") - for i in range(2000) - ] + rows = [(f"fast_{i:05d}", i % 256, f"data_{i}") for i in range(2000)] cursor = conn.cursor() @@ -169,7 +160,7 @@ def batch_insert_single_commit(conn: pycubrid.Connection) -> None: conn.commit() elapsed = time.perf_counter() - t0 - print(f"single commit: inserted {len(rows)} rows in {elapsed*1000:.1f}ms") + print(f"single commit: inserted {len(rows)} rows in {elapsed * 1000:.1f}ms") cursor.close() @@ -190,10 +181,7 @@ def batch_update(conn: pycubrid.Connection) -> None: Python โ€” same executemany, works for UPDATE too: """ - updates = [ - (999, f"sensor_{i:04d}") - for i in range(0, 100, 10) - ] + updates = [(999, f"sensor_{i:04d}") for i in range(0, 100, 10)] cursor = conn.cursor() cursor.executemany( diff --git a/quickstart/5min-fastapi/app.py b/quickstart/5min-fastapi/app.py index d20e8e2..4a9ddf1 100644 --- a/quickstart/5min-fastapi/app.py +++ b/quickstart/5min-fastapi/app.py @@ -6,12 +6,15 @@ app = FastAPI(title="CUBRID Quickstart API") + class ItemIn(BaseModel): val: str + class ItemOut(ItemIn): id: int + def get_conn() -> pycubrid.Connection: return pycubrid.connect( host="cubrid", @@ -21,6 +24,7 @@ def get_conn() -> pycubrid.Connection: password="", ) + @app.on_event("startup") def create_table() -> None: conn = get_conn() @@ -37,6 +41,7 @@ def create_table() -> None: cursor.close() conn.close() + @app.get("/items", response_model=list[ItemOut]) def list_items() -> list[ItemOut]: conn = get_conn() @@ -47,6 +52,7 @@ def list_items() -> list[ItemOut]: conn.close() return [ItemOut(id=row[0], val=row[1]) for row in rows] + @app.post("/items", response_model=ItemOut) def create_item(item: ItemIn) -> ItemOut: conn = get_conn() @@ -58,6 +64,7 @@ def create_item(item: ItemIn) -> ItemOut: conn.close() return ItemOut(id=item_id, val=item.val) + @app.get("/items/{item_id}", response_model=ItemOut) def get_item(item_id: int) -> ItemOut: conn = get_conn() diff --git a/quickstart/5min-sqlalchemy/app.py b/quickstart/5min-sqlalchemy/app.py index 7ecf665..9bdeca5 100644 --- a/quickstart/5min-sqlalchemy/app.py +++ b/quickstart/5min-sqlalchemy/app.py @@ -63,7 +63,9 @@ def run_core_crud(engine) -> None: ) conn.execute(update_stmt, {"update_item_name": "core-sample", "new_val": "v2"}) - delete_stmt = delete(core_items).where(core_items.c.item_name == bindparam("delete_item_name")) + delete_stmt = delete(core_items).where( + core_items.c.item_name == bindparam("delete_item_name") + ) conn.execute(delete_stmt, {"delete_item_name": "core-sample"}) @@ -76,7 +78,9 @@ def run_orm_crud(engine) -> None: session.add(orm_item) with Session(engine) as session: - query_stmt = select(CookbookOrmItem).where(CookbookOrmItem.item_name == bindparam("search_item_name")) + query_stmt = select(CookbookOrmItem).where( + CookbookOrmItem.item_name == bindparam("search_item_name") + ) found = session.execute(query_stmt, {"search_item_name": "orm-sample"}).scalar_one_or_none() print( "ORM query result:", @@ -90,7 +94,9 @@ def run_orm_crud(engine) -> None: with Session(engine) as session: with session.begin(): to_update = session.execute( - select(CookbookOrmItem).where(CookbookOrmItem.item_name == bindparam("update_item_name")), + select(CookbookOrmItem).where( + CookbookOrmItem.item_name == bindparam("update_item_name") + ), {"update_item_name": "orm-sample"}, ).scalar_one_or_none() if to_update is not None: @@ -99,7 +105,9 @@ def run_orm_crud(engine) -> None: with Session(engine) as session: with session.begin(): to_delete = session.execute( - select(CookbookOrmItem).where(CookbookOrmItem.item_name == bindparam("delete_item_name")), + select(CookbookOrmItem).where( + CookbookOrmItem.item_name == bindparam("delete_item_name") + ), {"delete_item_name": "orm-sample"}, ).scalar_one_or_none() if to_delete is not None: