Skip to content

Commit 926bafe

Browse files
improve: expand AGENTS.md with CI/CD, agent workflow, and business context (#32)
* improve: expand AGENTS.md with CI/CD, agent workflow, and business context * CI: SHA-pin actions and run the npm-wrapper test * fix: escape embedded quotes in identifiers to prevent SQL injection by dev-engineer * fix: add tests/__init__.py to make test package explicit by dev-engineer
1 parent 7a187e2 commit 926bafe

6 files changed

Lines changed: 106 additions & 23 deletions

File tree

.github/workflows/ci.yml

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ jobs:
1717
python-version: ["3.10", "3.11", "3.12", "3.13"]
1818

1919
steps:
20-
- uses: actions/checkout@v4
20+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
2121
with:
2222
persist-credentials: false
2323

2424
- name: Set up Python ${{ matrix.python-version }}
25-
uses: actions/setup-python@v5
25+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
2626
with:
2727
python-version: ${{ matrix.python-version }}
2828

@@ -36,3 +36,18 @@ jobs:
3636
- name: Run tests
3737
run: |
3838
python -m pytest tests/ -v --cov=src --cov-report=term-missing
39+
40+
js-wrapper:
41+
runs-on: ubuntu-latest
42+
steps:
43+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
44+
with:
45+
persist-credentials: false
46+
47+
- name: Set up Node
48+
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4
49+
with:
50+
node-version: "20"
51+
52+
- name: Test the npm wrapper
53+
run: node --test tests/*.test.js

AGENTS.md

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,35 @@
44
This repo converts JSON to SQL. Primary implementation is under `src/json2sql/`. CLI entrypoints live in `cli.py` and `__main__.py`.
55

66
## Commands
7-
- Install/dev: `pip install -e .`
8-
- Tests: `pytest`
9-
- Lint/type checks (if configured): use tooling in `pyproject.toml`
7+
- Install/dev: `pip install -e ".[dev]"`
8+
- Tests: `pytest tests/ -v`
9+
- Lint: `ruff check src/ tests/`
10+
- Format: `ruff format src/ tests/`
11+
- Type check: `mypy src/` (if configured)
12+
13+
## CI/CD
14+
- **CI** (`.github/workflows/ci.yml`): Tests on Python 3.10-3.13 + ruff lint
15+
- **Auto Code Review** (`.github/workflows/auto-code-review.yml`): Reusable org workflow
16+
- **Pages** (`.github/workflows/pages.yml`): Deploy docs to GitHub Pages
17+
- **Publish** (`.github/workflows/publish.yml`): PyPI publish on tags
18+
19+
## Agent workflow
20+
1. `git checkout main && git pull origin main`
21+
2. `git checkout -b improve/json2sql-<YYYYMMDD>`
22+
3. Make changes (max 50 lines per run)
23+
4. `ruff check src/ tests/ && ruff format src/ tests/`
24+
5. `pytest tests/ -v` — ensure all tests pass
25+
6. `git add -A && git commit -m "improve: <description>"`
26+
7. `git push origin improve/json2sql-<YYYYMMDD>`
27+
8. `gh pr create --title "improve: <description>" --body "Automated improvement by dev-engineer." --repo Coding-Dev-Tools/json2sql`
1028

1129
## Do not break
1230
- Do not remove or weaken existing tests.
1331
- Keep public CLI behavior stable unless an issue explicitly requests changing it.
32+
- Do not change the `convert()` function signature or return type.
33+
- Keep lazy imports inside `convert()` to preserve cold-start optimization.
34+
35+
## Business context
36+
- Part of **Coding-Dev-Tools** under **Revenue Holdings**
37+
- Revenue Holdings north star: "Generate revenue through CLI tools, SaaS products, and automated operations."
38+
- This is a Tier 2 repo (developer tool / CLI utility)

src/json2sql/dialects.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,20 @@ def sql_type_for(value: Any, dialect: Dialect) -> str:
4747

4848

4949
def quote_identifier(name: str, dialect: Dialect) -> str:
50-
"""Quote an identifier (table/column name) for the given dialect."""
50+
"""Quote an identifier (table/column name) for the given dialect.
51+
52+
Embedded quote characters are escaped by doubling — the standard SQL rule
53+
for quoted identifiers — mirroring how ``format_value`` escapes string
54+
literals. Without this, a table/column name (which comes straight from
55+
untrusted JSON object keys) containing a ``"`` (Postgres/SQLite) or a
56+
backtick (MySQL) could break out of the quoted identifier and inject
57+
arbitrary SQL into the generated statement.
58+
"""
5159
if dialect == Dialect.MYSQL:
52-
return f"`{name}`"
53-
return f'"{name}"'
60+
escaped = name.replace("`", "``")
61+
return f"`{escaped}`"
62+
escaped = name.replace('"', '""')
63+
return f'"{escaped}"'
5464

5565

5666
def format_value(value: Any, dialect: Dialect) -> str:

tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Tests for json2sql."""

tests/test_converter.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,7 @@ def test_flatten_nested_array_column_value_count(self):
174174
if cols and vals:
175175
c_count = len([c for c in cols.group(1).split(",") if c.strip()])
176176
v_count = len([v for v in vals.group(1).split(",") if v.strip()])
177-
assert c_count == v_count, (
178-
f"Column count ({c_count}) != value count ({v_count})"
179-
)
177+
assert c_count == v_count, f"Column count ({c_count}) != value count ({v_count})"
180178

181179
def test_flatten_mixed_nested_array_and_object_count(self):
182180
"""
@@ -201,9 +199,7 @@ def test_flatten_mixed_nested_array_and_object_count(self):
201199
if cols and vals:
202200
c_count = len([c for c in cols.group(1).split(",") if c.strip()])
203201
v_count = len([v for v in vals.group(1).split(",") if v.strip()])
204-
assert c_count == v_count, (
205-
f"Column count ({c_count}) != value count ({v_count})"
206-
)
202+
assert c_count == v_count, f"Column count ({c_count}) != value count ({v_count})"
207203

208204

209205
class TestFlattenDetail:
@@ -401,6 +397,28 @@ def test_string_with_quotes(self, converter_postgres):
401397
result = converter_postgres.convert(data, table_name="profiles")
402398
assert "It''s a test" in result # SQL-escaped single quote
403399

400+
def test_key_with_embedded_double_quote_postgres(self, converter_postgres):
401+
# A JSON key containing " must be escaped in the generated identifier
402+
# to prevent SQL injection through untrusted object keys.
403+
data = json.dumps({'col"evil': 1})
404+
result = converter_postgres.convert(data, table_name="profiles")
405+
assert '"col""evil"' in result
406+
# Ensure the dangerous unescaped form is NOT present
407+
assert '"col"evil"' not in result
408+
409+
def test_key_with_embedded_double_quote_sqlite(self, converter_sqlite):
410+
data = json.dumps({'col"evil': 1})
411+
result = converter_sqlite.convert(data, table_name="profiles")
412+
assert '"col""evil"' in result
413+
assert '"col"evil"' not in result
414+
415+
def test_key_with_embedded_backtick_mysql(self, converter_mysql):
416+
# A JSON key containing a backtick must be escaped in MySQL identifiers.
417+
data = json.dumps({"col`evil": 1})
418+
result = converter_mysql.convert(data, table_name="profiles")
419+
assert "`col``evil`" in result
420+
assert "`col`evil`" not in result
421+
404422
def test_float_values(self, converter_postgres):
405423
data = json.dumps({"price": 19.99})
406424
result = converter_postgres.convert(data, table_name="products")
@@ -515,6 +533,5 @@ def test_version_in_init_matches_pyproject(self):
515533
with open(pyproject, "rb") as f:
516534
data = tomllib.load(f)
517535
assert data["project"]["version"] == __version__, (
518-
f"pyproject.toml version ({data['project']['version']}) != "
519-
f"__init__.__version__ ({__version__})"
536+
f"pyproject.toml version ({data['project']['version']}) != __init__.__version__ ({__version__})"
520537
)

tests/test_dialects.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,9 @@ def test_all_members(self):
3636
class TestSQLTypeFor:
3737
"""Python type → SQL type mapping per dialect."""
3838

39-
@pytest.mark.parametrize(
40-
"dialect", [Dialect.POSTGRES, Dialect.MYSQL, Dialect.SQLITE]
41-
)
39+
@pytest.mark.parametrize("dialect", [Dialect.POSTGRES, Dialect.MYSQL, Dialect.SQLITE])
4240
def test_string_type(self, dialect):
43-
assert (
44-
"TEXT" in sql_type_for("hello", dialect).upper()
45-
or "VARCHAR" in sql_type_for("hello", dialect).upper()
46-
)
41+
assert "TEXT" in sql_type_for("hello", dialect).upper() or "VARCHAR" in sql_type_for("hello", dialect).upper()
4742

4843
def test_postgres_int(self):
4944
assert sql_type_for(42, Dialect.POSTGRES) == "INTEGER"
@@ -115,6 +110,26 @@ def test_empty_name(self):
115110
result = quote_identifier("", dialect)
116111
assert len(result) >= 2
117112

113+
def test_embedded_quote_mysql(self):
114+
"""MySQL: backtick embedded in identifier is escaped by doubling."""
115+
assert quote_identifier("my`table", Dialect.MYSQL) == "`my``table`"
116+
117+
def test_embedded_quote_postgres(self):
118+
"""Postgres: double-quote embedded in identifier is escaped by doubling."""
119+
assert quote_identifier('my"table', Dialect.POSTGRES) == '"my""table"'
120+
121+
def test_embedded_quote_sqlite(self):
122+
"""SQLite: double-quote embedded in identifier is escaped by doubling."""
123+
assert quote_identifier('my"table', Dialect.SQLITE) == '"my""table"'
124+
125+
def test_multiple_embedded_quotes_mysql(self):
126+
"""MySQL: multiple backticks in identifier are all escaped."""
127+
assert quote_identifier("a`b`c", Dialect.MYSQL) == "`a``b``c`"
128+
129+
def test_multiple_embedded_quotes_postgres(self):
130+
"""Postgres: multiple double-quotes in identifier are all escaped."""
131+
assert quote_identifier('a"b"c', Dialect.POSTGRES) == '"a""b""c"'
132+
118133

119134
# --- format_value ---
120135

0 commit comments

Comments
 (0)