json2sql-sql 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- json2sql-sql/__init__.py +3 -0
- json2sql-sql/converter.py +80 -0
- json2sql-sql/exceptions.py +10 -0
- json2sql-sql/exporter.py +11 -0
- json2sql-sql/flattener.py +31 -0
- json2sql-sql/generator.py +48 -0
- json2sql-sql/loader.py +38 -0
- json2sql-sql/schema.py +74 -0
- json2sql-sql/utils.py +40 -0
- json2sql_sql-0.1.2.dist-info/METADATA +24 -0
- json2sql_sql-0.1.2.dist-info/RECORD +15 -0
- json2sql_sql-0.1.2.dist-info/WHEEL +5 -0
- json2sql_sql-0.1.2.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_basic.py +17 -0
json2sql-sql/__init__.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
|
+
|
|
3
|
+
from .loader import load_json, normalize_json_data
|
|
4
|
+
from .flattener import flatten_json
|
|
5
|
+
from .schema import detect_schema
|
|
6
|
+
from .generator import generate_create_table_query, generate_insert_queries
|
|
7
|
+
from .exporter import export_sql
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Converter:
|
|
11
|
+
"""
|
|
12
|
+
Main class for converting JSON data into SQL queries.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, source: Any, flatten: bool = True, separator: str = "_") -> None:
|
|
16
|
+
"""
|
|
17
|
+
source can be:
|
|
18
|
+
- file path to JSON
|
|
19
|
+
- dict
|
|
20
|
+
- list of dicts
|
|
21
|
+
"""
|
|
22
|
+
self.source = source
|
|
23
|
+
self.flatten = flatten
|
|
24
|
+
self.separator = separator
|
|
25
|
+
|
|
26
|
+
self.records: List[Dict[str, Any]] = []
|
|
27
|
+
self.schema: Dict[str, str] = {}
|
|
28
|
+
self.table_name: Optional[str] = None
|
|
29
|
+
self.create_query: Optional[str] = None
|
|
30
|
+
self.insert_queries: List[str] = []
|
|
31
|
+
|
|
32
|
+
self._load_and_prepare()
|
|
33
|
+
|
|
34
|
+
def _load_and_prepare(self) -> None:
|
|
35
|
+
if isinstance(self.source, str):
|
|
36
|
+
raw_records = load_json(self.source)
|
|
37
|
+
else:
|
|
38
|
+
raw_records = normalize_json_data(self.source)
|
|
39
|
+
|
|
40
|
+
if self.flatten:
|
|
41
|
+
self.records = [flatten_json(record, sep=self.separator) for record in raw_records]
|
|
42
|
+
else:
|
|
43
|
+
self.records = raw_records
|
|
44
|
+
|
|
45
|
+
self.schema = detect_schema(self.records)
|
|
46
|
+
|
|
47
|
+
def create_table(self, table_name: str) -> str:
|
|
48
|
+
self.table_name = table_name
|
|
49
|
+
self.create_query = generate_create_table_query(table_name, self.schema)
|
|
50
|
+
return self.create_query
|
|
51
|
+
|
|
52
|
+
def insert_data(self, table_name: Optional[str] = None) -> List[str]:
|
|
53
|
+
if table_name:
|
|
54
|
+
self.table_name = table_name
|
|
55
|
+
|
|
56
|
+
if not self.table_name:
|
|
57
|
+
raise ValueError("Table name is not set. Call create_table(table_name) first.")
|
|
58
|
+
|
|
59
|
+
self.insert_queries = generate_insert_queries(self.table_name, self.records, self.schema)
|
|
60
|
+
return self.insert_queries
|
|
61
|
+
|
|
62
|
+
def convert(self, table_name: str) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Generate full SQL as a single string.
|
|
65
|
+
"""
|
|
66
|
+
create_query = self.create_table(table_name)
|
|
67
|
+
insert_queries = self.insert_data()
|
|
68
|
+
|
|
69
|
+
sql_parts = [create_query, ""]
|
|
70
|
+
sql_parts.extend(insert_queries)
|
|
71
|
+
return "\n".join(sql_parts)
|
|
72
|
+
|
|
73
|
+
def export(self, output_path: str) -> None:
|
|
74
|
+
if not self.create_query:
|
|
75
|
+
raise ValueError("Create query not generated. Call create_table(table_name) first.")
|
|
76
|
+
|
|
77
|
+
if not self.insert_queries:
|
|
78
|
+
raise ValueError("Insert queries not generated. Call insert_data() first.")
|
|
79
|
+
|
|
80
|
+
export_sql(output_path, self.create_query, self.insert_queries)
|
json2sql-sql/exporter.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def export_sql(output_path: str, create_query: str, insert_queries: List[str]) -> None:
|
|
5
|
+
"""
|
|
6
|
+
Export SQL queries to a .sql file.
|
|
7
|
+
"""
|
|
8
|
+
with open(output_path, "w", encoding="utf-8") as file:
|
|
9
|
+
file.write(create_query + "\n\n")
|
|
10
|
+
for query in insert_queries:
|
|
11
|
+
file.write(query + "\n")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from typing import Any, Dict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def flatten_json(data: Dict[str, Any], parent_key: str = "", sep: str = "_") -> Dict[str, Any]:
|
|
5
|
+
"""
|
|
6
|
+
Flatten nested JSON.
|
|
7
|
+
Example:
|
|
8
|
+
{
|
|
9
|
+
"id": 1,
|
|
10
|
+
"address": {"city": "Chennai"}
|
|
11
|
+
}
|
|
12
|
+
becomes:
|
|
13
|
+
{
|
|
14
|
+
"id": 1,
|
|
15
|
+
"address_city": "Chennai"
|
|
16
|
+
}
|
|
17
|
+
"""
|
|
18
|
+
items: Dict[str, Any] = {}
|
|
19
|
+
|
|
20
|
+
for key, value in data.items():
|
|
21
|
+
new_key = f"{parent_key}{sep}{key}" if parent_key else key
|
|
22
|
+
|
|
23
|
+
if isinstance(value, dict):
|
|
24
|
+
items.update(flatten_json(value, new_key, sep))
|
|
25
|
+
elif isinstance(value, list):
|
|
26
|
+
# Store lists as JSON-like strings in first version
|
|
27
|
+
items[new_key] = str(value)
|
|
28
|
+
else:
|
|
29
|
+
items[new_key] = value
|
|
30
|
+
|
|
31
|
+
return items
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from typing import Any, Dict, List
|
|
2
|
+
|
|
3
|
+
from .utils import sanitize_identifier, sql_escape
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate_create_table_query(table_name: str, schema: Dict[str, str]) -> str:
|
|
7
|
+
"""
|
|
8
|
+
Generate CREATE TABLE query.
|
|
9
|
+
"""
|
|
10
|
+
safe_table = sanitize_identifier(table_name)
|
|
11
|
+
|
|
12
|
+
columns = []
|
|
13
|
+
for column_name, sql_type in schema.items():
|
|
14
|
+
safe_column = sanitize_identifier(column_name)
|
|
15
|
+
columns.append(f" {safe_column} {sql_type}")
|
|
16
|
+
|
|
17
|
+
columns_sql = ",\n".join(columns)
|
|
18
|
+
|
|
19
|
+
return f"CREATE TABLE {safe_table} (\n{columns_sql}\n);"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def generate_insert_queries(table_name: str, records: List[Dict[str, Any]], schema: Dict[str, str]) -> List[str]:
|
|
23
|
+
"""
|
|
24
|
+
Generate INSERT INTO queries.
|
|
25
|
+
"""
|
|
26
|
+
safe_table = sanitize_identifier(table_name)
|
|
27
|
+
ordered_columns = list(schema.keys())
|
|
28
|
+
queries: List[str] = []
|
|
29
|
+
|
|
30
|
+
for record in records:
|
|
31
|
+
values = []
|
|
32
|
+
for column in ordered_columns:
|
|
33
|
+
original_value = None
|
|
34
|
+
|
|
35
|
+
for key, value in record.items():
|
|
36
|
+
if sanitize_identifier(key) == column:
|
|
37
|
+
original_value = value
|
|
38
|
+
break
|
|
39
|
+
|
|
40
|
+
values.append(sql_escape(original_value))
|
|
41
|
+
|
|
42
|
+
columns_sql = ", ".join(ordered_columns)
|
|
43
|
+
values_sql = ", ".join(values)
|
|
44
|
+
|
|
45
|
+
query = f"INSERT INTO {safe_table} ({columns_sql}) VALUES ({values_sql});"
|
|
46
|
+
queries.append(query)
|
|
47
|
+
|
|
48
|
+
return queries
|
json2sql-sql/loader.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any, List, Dict
|
|
3
|
+
|
|
4
|
+
from .exceptions import InvalidJsonError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def load_json(source: str) -> List[Dict[str, Any]]:
|
|
8
|
+
"""
|
|
9
|
+
Load JSON data from a file path.
|
|
10
|
+
|
|
11
|
+
Supported input:
|
|
12
|
+
- list of dictionaries
|
|
13
|
+
- single dictionary (converted to list with one item)
|
|
14
|
+
"""
|
|
15
|
+
try:
|
|
16
|
+
with open(source, "r", encoding="utf-8") as file:
|
|
17
|
+
data = json.load(file)
|
|
18
|
+
except FileNotFoundError as exc:
|
|
19
|
+
raise InvalidJsonError(f"JSON file not found: {source}") from exc
|
|
20
|
+
except json.JSONDecodeError as exc:
|
|
21
|
+
raise InvalidJsonError(f"Invalid JSON format in file: {source}") from exc
|
|
22
|
+
|
|
23
|
+
return normalize_json_data(data)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def normalize_json_data(data: Any) -> List[Dict[str, Any]]:
|
|
27
|
+
"""
|
|
28
|
+
Normalize JSON input into a list of dictionaries.
|
|
29
|
+
"""
|
|
30
|
+
if isinstance(data, dict):
|
|
31
|
+
return [data]
|
|
32
|
+
|
|
33
|
+
if isinstance(data, list):
|
|
34
|
+
if all(isinstance(item, dict) for item in data):
|
|
35
|
+
return data
|
|
36
|
+
raise InvalidJsonError("JSON list must contain only objects/dictionaries.")
|
|
37
|
+
|
|
38
|
+
raise InvalidJsonError("JSON must be a dictionary or a list of dictionaries.")
|
json2sql-sql/schema.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from typing import Any, Dict, List
|
|
2
|
+
|
|
3
|
+
from .exceptions import SchemaDetectionError
|
|
4
|
+
from .utils import sanitize_identifier
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def detect_sql_type(value: Any) -> str:
|
|
8
|
+
"""
|
|
9
|
+
Detect SQL type from Python value.
|
|
10
|
+
"""
|
|
11
|
+
if value is None:
|
|
12
|
+
return "TEXT"
|
|
13
|
+
|
|
14
|
+
if isinstance(value, bool):
|
|
15
|
+
return "BOOLEAN"
|
|
16
|
+
|
|
17
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
18
|
+
return "INTEGER"
|
|
19
|
+
|
|
20
|
+
if isinstance(value, float):
|
|
21
|
+
return "REAL"
|
|
22
|
+
|
|
23
|
+
return "TEXT"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def merge_sql_types(type1: str, type2: str) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Merge conflicting SQL types into a safer common type.
|
|
29
|
+
"""
|
|
30
|
+
priority = ["BOOLEAN", "INTEGER", "REAL", "TEXT"]
|
|
31
|
+
|
|
32
|
+
if type1 == type2:
|
|
33
|
+
return type1
|
|
34
|
+
|
|
35
|
+
# If either is TEXT, safest is TEXT
|
|
36
|
+
if "TEXT" in (type1, type2):
|
|
37
|
+
return "TEXT"
|
|
38
|
+
|
|
39
|
+
# INTEGER + REAL => REAL
|
|
40
|
+
if {"INTEGER", "REAL"} == {type1, type2}:
|
|
41
|
+
return "REAL"
|
|
42
|
+
|
|
43
|
+
# BOOLEAN mixed with anything else => TEXT
|
|
44
|
+
if "BOOLEAN" in (type1, type2):
|
|
45
|
+
return "TEXT"
|
|
46
|
+
|
|
47
|
+
return priority[max(priority.index(type1), priority.index(type2))]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def detect_schema(records: List[Dict[str, Any]]) -> Dict[str, str]:
|
|
51
|
+
"""
|
|
52
|
+
Detect schema from list of flattened records.
|
|
53
|
+
Returns dict like:
|
|
54
|
+
{
|
|
55
|
+
"id": "INTEGER",
|
|
56
|
+
"name": "TEXT"
|
|
57
|
+
}
|
|
58
|
+
"""
|
|
59
|
+
if not records:
|
|
60
|
+
raise SchemaDetectionError("No records found for schema detection.")
|
|
61
|
+
|
|
62
|
+
schema: Dict[str, str] = {}
|
|
63
|
+
|
|
64
|
+
for record in records:
|
|
65
|
+
for key, value in record.items():
|
|
66
|
+
safe_key = sanitize_identifier(key)
|
|
67
|
+
current_type = detect_sql_type(value)
|
|
68
|
+
|
|
69
|
+
if safe_key in schema:
|
|
70
|
+
schema[safe_key] = merge_sql_types(schema[safe_key], current_type)
|
|
71
|
+
else:
|
|
72
|
+
schema[safe_key] = current_type
|
|
73
|
+
|
|
74
|
+
return schema
|
json2sql-sql/utils.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def sanitize_identifier(name: str) -> str:
|
|
6
|
+
"""
|
|
7
|
+
Convert any string into a safe SQL identifier.
|
|
8
|
+
Example: 'user name' -> 'user_name'
|
|
9
|
+
"""
|
|
10
|
+
if not isinstance(name, str):
|
|
11
|
+
name = str(name)
|
|
12
|
+
|
|
13
|
+
name = name.strip().lower()
|
|
14
|
+
name = re.sub(r"[^a-zA-Z0-9_]", "_", name)
|
|
15
|
+
name = re.sub(r"_+", "_", name)
|
|
16
|
+
|
|
17
|
+
if not name:
|
|
18
|
+
name = "column"
|
|
19
|
+
|
|
20
|
+
if name[0].isdigit():
|
|
21
|
+
name = f"col_{name}"
|
|
22
|
+
|
|
23
|
+
return name
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def sql_escape(value: Any) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Escape Python values for SQL INSERT statements.
|
|
29
|
+
"""
|
|
30
|
+
if value is None:
|
|
31
|
+
return "NULL"
|
|
32
|
+
|
|
33
|
+
if isinstance(value, bool):
|
|
34
|
+
return "TRUE" if value else "FALSE"
|
|
35
|
+
|
|
36
|
+
if isinstance(value, (int, float)):
|
|
37
|
+
return str(value)
|
|
38
|
+
|
|
39
|
+
value = str(value).replace("'", "''")
|
|
40
|
+
return f"'{value}'"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: json2sql-sql
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Convert JSON data into SQL queries
|
|
5
|
+
Author: NaveenKumar
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Dynamic: requires-python
|
|
9
|
+
|
|
10
|
+
# json2sql
|
|
11
|
+
|
|
12
|
+
A simple Python library to convert JSON data into SQL queries.
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- Auto table creation
|
|
17
|
+
- Insert query generation
|
|
18
|
+
- Schema detection
|
|
19
|
+
- Nested JSON flattening
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install -r requirements.txt
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
json2sql-sql/__init__.py,sha256=AhwSX-wiMz6_eRZw9fZWNdV0UxwpNWWTRqw5tQhL_Io,59
|
|
2
|
+
json2sql-sql/converter.py,sha256=x4huzNq2bZBi5D_iRwo7Gg1Th17OTGCxze-tSpa33Yk,2751
|
|
3
|
+
json2sql-sql/exceptions.py,sha256=ZaxZKe6Y5KxKM6efa_DWD1dKudcCanFnGpUMtItKkMY,264
|
|
4
|
+
json2sql-sql/exporter.py,sha256=rDRSa3WgdSOpifqsb7U0MgCXcRMIOY0wKzS47ngI5rM,353
|
|
5
|
+
json2sql-sql/flattener.py,sha256=nzZjovL0yFoqkyIhArGGxlGw-s-j1bxLa9NwgYwsKhM,795
|
|
6
|
+
json2sql-sql/generator.py,sha256=Uiwi41Qw9TUaj5tsmKS7HnGGIuN1CUi5I3pWRlP0Zig,1448
|
|
7
|
+
json2sql-sql/loader.py,sha256=ZX6C-_Dx-8iQGvm5IosAwE7CPt6-e6Bu_VEDRlmEoTs,1178
|
|
8
|
+
json2sql-sql/schema.py,sha256=EaHFIEdDM_kR2H9DAnuNdAvl91jUXOYu_l552rOAQ40,1875
|
|
9
|
+
json2sql-sql/utils.py,sha256=DyK4jmE_0c3xs_EmyJBbFTGVvg795J-NkePg6kQIAUQ,873
|
|
10
|
+
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
tests/test_basic.py,sha256=PqTYfTs61aXeA8wOjKNsPdvOehqv6fWjVbQnzGNlH6U,479
|
|
12
|
+
json2sql_sql-0.1.2.dist-info/METADATA,sha256=wF_yLn7OwG6UzxEoaKRZx6Tguulht5d95fhxtM1lqYs,472
|
|
13
|
+
json2sql_sql-0.1.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
14
|
+
json2sql_sql-0.1.2.dist-info/top_level.txt,sha256=3VbAySvqtpdFa4jPTdJ-qPiRurynhaGylH-H654GulY,19
|
|
15
|
+
json2sql_sql-0.1.2.dist-info/RECORD,,
|
tests/__init__.py
ADDED
|
File without changes
|
tests/test_basic.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from json2sql import Converter
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_basic_conversion():
|
|
5
|
+
data = [
|
|
6
|
+
{"id": 1, "name": "Naveen", "address": {"city": "Chennai"}},
|
|
7
|
+
{"id": 2, "name": "Kumar", "address": {"city": "Madurai"}}
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
converter = Converter(data)
|
|
11
|
+
sql = converter.convert("users")
|
|
12
|
+
|
|
13
|
+
assert "CREATE TABLE users" in sql
|
|
14
|
+
assert "id INTEGER" in sql
|
|
15
|
+
assert "name TEXT" in sql
|
|
16
|
+
assert "address_city TEXT" in sql
|
|
17
|
+
assert "INSERT INTO users" in sql
|