minimew commited on
Commit
569d5fe
Β·
verified Β·
1 Parent(s): 02b7435

Upload src/process_spider.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. src/process_spider.py +338 -0
src/process_spider.py ADDED
@@ -0,0 +1,338 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Spider Data Processing Script v5
3
+ Build schema-aware inputs for Spider to fine-tune T5/CodeT5/BART.
4
+
5
+ Input: spider_data/{train_spider,dev,test}.json + tables.json + test_tables.json
6
+ Output: processed/spider/{train,dev,test}.json
7
+
8
+ Usage:
9
+ python src/process_spider.py
10
+ python src/process_spider.py --no_foreign_keys
11
+ python src/process_spider.py --no_column_types
12
+ python src/process_spider.py --no_primary_keys
13
+ python src/process_spider.py --no_lowercase_identifiers
14
+ python src/process_spider.py --oracle_schema_link --output_dir processed/spider_oracle
15
+ (RQ6 ablation: filter schema to keep only tables used in gold SQL β€” ceiling-effect
16
+ study for WRONG_TABLE error; not applied to test split which has no gold SQL)
17
+ """
18
+
19
+ import re
20
+ import json
21
+ import argparse
22
+ import sys
23
+ from pathlib import Path
24
+
25
+ try:
26
+ sys.stdout.reconfigure(encoding='utf-8')
27
+ except Exception:
28
+ pass
29
+
30
+ try:
31
+ import sqlglot
32
+ import sqlglot.expressions as exp
33
+ HAS_SQLGLOT = True
34
+ except ImportError:
35
+ HAS_SQLGLOT = False
36
+ print("WARNING: sqlglot not installed. Run: pip install sqlglot")
37
+
38
+
39
+ # ── SQL utilities ──────────────────────────────────────────────────────────────
40
+
41
+ def normalize_sql(sql):
42
+ """Normalize SQL for Exact Match β€” preserve string literals (Spider DB is mixed-case)."""
43
+ if not sql:
44
+ return ""
45
+ if HAS_SQLGLOT:
46
+ try:
47
+ return sqlglot.parse_one(sql, read='sqlite').sql(
48
+ dialect='sqlite', pretty=False
49
+ ).strip()
50
+ except Exception:
51
+ pass
52
+ sql = sql.strip()
53
+ sql = re.sub(r'\s+', ' ', sql)
54
+ sql = re.sub(r'\s*,\s*', ' , ', sql)
55
+ sql = re.sub(r'\s*(=|!=|<>|>=|<=|>|<)\s*', r' \1 ', sql)
56
+ return sql.strip()
57
+
58
+
59
+ def normalize_question(question):
60
+ return re.sub(r'\s+', ' ', question.strip()).rstrip('.?!').strip()
61
+
62
+
63
+ def normalize_identifier(name, lowercase=True):
64
+ return name.strip().lower() if lowercase else name.strip()
65
+
66
+
67
+ def normalize_primary_keys(primary_keys):
68
+ pk_indices = set()
69
+ for pk in primary_keys:
70
+ if isinstance(pk, list):
71
+ pk_indices.update(pk)
72
+ else:
73
+ pk_indices.add(pk)
74
+ return pk_indices
75
+
76
+
77
+ def check_sql_validity(sql):
78
+ if not sql or not sql.strip():
79
+ return False
80
+ if HAS_SQLGLOT:
81
+ try:
82
+ sqlglot.parse_one(sql, read='sqlite')
83
+ return True
84
+ except Exception:
85
+ return False
86
+ return sql.strip().upper().startswith('SELECT')
87
+
88
+
89
+ # ── Schema builder ─────────────────────────────────────────────────────────────
90
+
91
+ def build_schema_string(db, use_foreign_keys=True, use_column_types=True,
92
+ use_primary_keys=True, lowercase_identifiers=True):
93
+ table_names = [normalize_identifier(n, lowercase_identifiers)
94
+ for n in db['table_names_original']]
95
+ col_names = [(ti, normalize_identifier(cn, lowercase_identifiers))
96
+ for ti, cn in db['column_names_original']]
97
+ col_types = db.get('column_types', [])
98
+ primary_keys = normalize_primary_keys(db.get('primary_keys', []))
99
+
100
+ table_cols = {i: [] for i in range(len(table_names))}
101
+ for col_idx, (ti, cn) in enumerate(col_names):
102
+ if ti == -1:
103
+ continue
104
+ col = cn
105
+ if use_primary_keys and col_idx in primary_keys:
106
+ col += "*"
107
+ if use_column_types and col_idx < len(col_types):
108
+ col += f":{col_types[col_idx]}"
109
+ table_cols[ti].append(col)
110
+
111
+ parts = []
112
+ for i, tname in enumerate(table_names):
113
+ cols = table_cols.get(i, [])
114
+ parts.append(f"{tname} ( {' , '.join(cols)} )" if cols else tname)
115
+ schema = " | ".join(parts)
116
+
117
+ if use_foreign_keys and db.get('foreign_keys'):
118
+ fk_parts = []
119
+ for c1, c2 in db['foreign_keys']:
120
+ t1 = table_names[col_names[c1][0]]
121
+ t2 = table_names[col_names[c2][0]]
122
+ fk_parts.append(f"{t1}.{col_names[c1][1]} = {t2}.{col_names[c2][1]}")
123
+ if fk_parts:
124
+ schema += " | foreign_keys: " + " , ".join(fk_parts)
125
+
126
+ return schema
127
+
128
+
129
+ def build_model_input(question, db, use_foreign_keys=True, use_column_types=True,
130
+ use_primary_keys=True, lowercase_identifiers=True):
131
+ schema = build_schema_string(db, use_foreign_keys, use_column_types,
132
+ use_primary_keys, lowercase_identifiers)
133
+ return f"question: {normalize_question(question)} | schema: {schema}"
134
+
135
+
136
+ # ── Oracle schema-linking (RQ6 ablation) ────────────────────────────────────────
137
+ # Ceiling-effect study: use gold SQL to filter schema to only tables actually used.
138
+ # Measures WRONG_TABLE error reduction under perfect schema-linking.
139
+ # NOT a real inference system (requires gold SQL in advance) β€” train/dev only.
140
+
141
+ def extract_gold_tables(sql, table_names_original):
142
+ """Extract table names used in gold SQL via sqlglot AST (handles T1/T2 aliases
143
+ and nested subqueries). Falls back to all original tables on parse error or no
144
+ match β€” safe, never produces an empty schema."""
145
+ valid = {t.lower() for t in table_names_original}
146
+ if not HAS_SQLGLOT:
147
+ return valid
148
+ try:
149
+ tree = sqlglot.parse_one(sql, read='sqlite')
150
+ used = {t.name.lower() for t in tree.find_all(exp.Table)}
151
+ except Exception:
152
+ return valid
153
+ matched = used & valid
154
+ return matched if matched else valid
155
+
156
+
157
+ def filter_db_to_tables(db, keep_tables_lower):
158
+ """Filter db dict to keep only tables in keep_tables_lower, remapping all
159
+ column/table indices so build_schema_string() works unchanged.
160
+ Foreign keys are kept only when both columns belong to retained tables."""
161
+ table_names = db['table_names_original']
162
+ keep_indices = [i for i, t in enumerate(table_names) if t.lower() in keep_tables_lower]
163
+ if not keep_indices:
164
+ return db # an toan: khong loc duoc gi thi giu nguyen schema goc
165
+
166
+ index_map = {old: new for new, old in enumerate(keep_indices)}
167
+ new_table_names = [table_names[i] for i in keep_indices]
168
+
169
+ col_names = db['column_names_original']
170
+ col_types = db.get('column_types', [])
171
+ new_col_names, new_col_types, col_index_map = [], [], {}
172
+ for old_idx, (ti, cn) in enumerate(col_names):
173
+ if ti == -1 or ti in index_map:
174
+ new_ti = -1 if ti == -1 else index_map[ti]
175
+ col_index_map[old_idx] = len(new_col_names)
176
+ new_col_names.append([new_ti, cn])
177
+ if old_idx < len(col_types):
178
+ new_col_types.append(col_types[old_idx])
179
+
180
+ new_primary_keys = []
181
+ for pk in db.get('primary_keys', []):
182
+ if isinstance(pk, list):
183
+ filtered = [col_index_map[c] for c in pk if c in col_index_map]
184
+ if filtered:
185
+ new_primary_keys.append(filtered if len(filtered) > 1 else filtered[0])
186
+ elif pk in col_index_map:
187
+ new_primary_keys.append(col_index_map[pk])
188
+
189
+ new_foreign_keys = [
190
+ [col_index_map[c1], col_index_map[c2]]
191
+ for c1, c2 in db.get('foreign_keys', [])
192
+ if c1 in col_index_map and c2 in col_index_map
193
+ ]
194
+
195
+ return {
196
+ **db,
197
+ 'table_names_original': new_table_names,
198
+ 'column_names_original': new_col_names,
199
+ 'column_types': new_col_types,
200
+ 'primary_keys': new_primary_keys,
201
+ 'foreign_keys': new_foreign_keys,
202
+ }
203
+
204
+
205
+ # ── Data loading ───────────────────────────────────────────────────────────────
206
+
207
+ def load_schemas(tables_path):
208
+ with open(tables_path, 'r', encoding='utf-8') as f:
209
+ tables = json.load(f)
210
+ schemas = {db['db_id']: db for db in tables}
211
+ print(f" Loaded {len(schemas):,} schemas from {Path(tables_path).name}")
212
+ return schemas
213
+
214
+
215
+ # ── Process split ──────────────────────────────────────────────────────────────
216
+
217
+ def process_split(data_path, schemas, split_name,
218
+ use_foreign_keys=True, use_column_types=True,
219
+ use_primary_keys=True, lowercase_identifiers=True,
220
+ oracle_schema_link=False):
221
+ print(f"\n[{split_name.upper()}] Processing...")
222
+ with open(data_path, 'r', encoding='utf-8') as f:
223
+ data = json.load(f)
224
+
225
+ dataset, errors, not_found, invalid_sql = [], 0, 0, 0
226
+ is_test = (split_name == 'test')
227
+ # Oracle filtering only applies when gold SQL is available (train/dev).
228
+ # Test split has no gold SQL β€” skip.
229
+ apply_oracle = oracle_schema_link and not is_test
230
+ tables_before, tables_after = [], []
231
+
232
+ for idx, item in enumerate(data):
233
+ try:
234
+ db_id = item['db_id']
235
+ question = normalize_question(item['question'])
236
+ if db_id not in schemas:
237
+ not_found += 1
238
+ continue
239
+
240
+ db = schemas[db_id]
241
+ sql = item.get('query', item.get('SQL', '')).strip() if not is_test else ''
242
+
243
+ db_for_schema = db
244
+ if apply_oracle and sql:
245
+ keep_tables = extract_gold_tables(sql, db['table_names_original'])
246
+ db_for_schema = filter_db_to_tables(db, keep_tables)
247
+ tables_before.append(len(db['table_names_original']))
248
+ tables_after.append(len(db_for_schema['table_names_original']))
249
+
250
+ record = {
251
+ 'input': build_model_input(question, db_for_schema, use_foreign_keys,
252
+ use_column_types, use_primary_keys,
253
+ lowercase_identifiers),
254
+ 'question': question,
255
+ 'db_id': db_id,
256
+ }
257
+
258
+ if not is_test:
259
+ normalized_sql = normalize_sql(sql)
260
+ valid = check_sql_validity(sql)
261
+ if not valid:
262
+ invalid_sql += 1
263
+ record['output'] = normalized_sql
264
+ record['raw_output'] = sql
265
+ record['normalized_output'] = normalized_sql
266
+ record['is_valid_sql'] = valid
267
+ if 'query_toks' in item:
268
+ record['query_toks'] = item['query_toks']
269
+
270
+ dataset.append(record)
271
+ except Exception as e:
272
+ errors += 1
273
+ if errors <= 5:
274
+ print(f" WARNING idx {idx}: {e}")
275
+
276
+ print(f" Processed: {len(dataset):,} | Not found: {not_found} | "
277
+ f"Errors: {errors} | Invalid SQL: {invalid_sql}")
278
+ if apply_oracle and tables_before:
279
+ avg_before = sum(tables_before) / len(tables_before)
280
+ avg_after = sum(tables_after) / len(tables_after)
281
+ reduction = (1 - avg_after / avg_before) * 100 if avg_before else 0
282
+ print(f" [oracle_schema_link] Avg tables/schema: {avg_before:.2f} -> "
283
+ f"{avg_after:.2f} (-{reduction:.1f}%)")
284
+ return dataset
285
+
286
+
287
+ # ── Main ───────────────────────────────────────────────────────────────────────
288
+
289
+ def main():
290
+ parser = argparse.ArgumentParser(description='Process Spider v5')
291
+ parser.add_argument('--data_dir', default=r'C:\Users\ADMIN\Documents\Text to SQL\spider_data')
292
+ parser.add_argument('--output_dir', default=r'C:\Users\ADMIN\Documents\Text to SQL\processed\spider')
293
+ parser.add_argument('--no_foreign_keys', action='store_true', default=False)
294
+ parser.add_argument('--no_column_types', action='store_true', default=False)
295
+ parser.add_argument('--no_primary_keys', action='store_true', default=False)
296
+ parser.add_argument('--no_lowercase_identifiers', action='store_true', default=False)
297
+ parser.add_argument('--oracle_schema_link', action='store_true', default=False,
298
+ help='RQ6 ablation: filter schema to keep only tables in gold SQL '
299
+ '(ceiling-effect study for WRONG_TABLE error). Not applied to '
300
+ 'test split (no gold SQL available).')
301
+ args = parser.parse_args()
302
+
303
+ data_dir = Path(args.data_dir)
304
+ output_dir = Path(args.output_dir)
305
+ output_dir.mkdir(parents=True, exist_ok=True)
306
+
307
+ use_fk = not args.no_foreign_keys
308
+ use_column_types = not args.no_column_types
309
+ use_primary_keys = not args.no_primary_keys
310
+ lowercase_identifiers = not args.no_lowercase_identifiers
311
+ oracle_schema_link = args.oracle_schema_link
312
+
313
+ schemas = load_schemas(str(data_dir / 'tables.json'))
314
+ test_schemas = (load_schemas(str(data_dir / 'test_tables.json'))
315
+ if (data_dir / 'test_tables.json').exists() else schemas)
316
+
317
+ splits = [
318
+ ('train', data_dir / 'train_spider.json', schemas),
319
+ ('dev', data_dir / 'dev.json', schemas),
320
+ ('test', data_dir / 'test.json', test_schemas),
321
+ ]
322
+ for split_name, fpath, split_schemas in splits:
323
+ if not fpath.exists():
324
+ print(f"WARNING: {fpath} not found, skipping")
325
+ continue
326
+ dataset = process_split(str(fpath), split_schemas, split_name,
327
+ use_fk, use_column_types,
328
+ use_primary_keys, lowercase_identifiers,
329
+ oracle_schema_link)
330
+ out = output_dir / f"{split_name}.json"
331
+ with open(out, 'w', encoding='utf-8') as f:
332
+ json.dump(dataset, f, ensure_ascii=False, indent=2)
333
+ size = out.stat().st_size / 1e6
334
+ print(f" Saved -> {out} ({len(dataset):,} samples, {size:.1f} MB)")
335
+
336
+
337
+ if __name__ == '__main__':
338
+ main()