laudes commited on
Commit
2cb3f69
·
verified ·
1 Parent(s): 76192bd

Upload 8 files

Browse files
Files changed (6) hide show
  1. README.md +2 -3
  2. app.py +88 -41
  3. db.py +11 -1
  4. examples.yaml +74 -0
  5. openai_integration.py +108 -125
  6. schema.json +1651 -389
README.md CHANGED
@@ -2,6 +2,5 @@
2
  title: ai_eee_sql_gen
3
  app_file: app.py
4
  sdk: gradio
5
- sdk_version: 4.44.0
6
- python_version: 3.11.9
7
- ---
 
2
  title: ai_eee_sql_gen
3
  app_file: app.py
4
  sdk: gradio
5
+ sdk_version: 4.43.0
6
+ ---
 
app.py CHANGED
@@ -1,12 +1,21 @@
 
1
  import gradio as gr
2
  import logging
3
-
4
- from openai_integration import generate_sql
5
- from db import get_last_50_saved_queries, initialize_local_db, export_saved_queries_to_csv, execute_sql_query, fetch_and_save_schema, show_last_50_saved_queries # Import the correct function
 
 
 
 
 
 
 
 
6
 
7
 
8
  # Initialize logging
9
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname=s - %(message)s')
10
 
11
  # Call the function to ensure the table is created
12
  initialize_local_db()
@@ -16,16 +25,16 @@ def query_database(nl_query, progress=gr.Progress()):
16
  try:
17
  progress(0, desc="Starting Query Process")
18
 
19
- # Generate SQL and reformulated query
20
- progress(0.2, desc="Generating Reformulated Query")
21
- reformulated_query, sql_query = generate_sql(nl_query)
22
 
23
  # Default empty result in case of SQL query failure
24
  execution_result = []
25
 
26
  # If we have a SQL query, attempt execution
27
  if sql_query and not sql_query.startswith("Error"):
28
- progress(0.5, desc="Executing SQL Query")
29
  execution_result = execute_sql_query(sql_query)
30
 
31
  # Ensure execution_result is in a valid format for a DataFrame
@@ -35,11 +44,11 @@ def query_database(nl_query, progress=gr.Progress()):
35
  execution_result = [["No results available."]]
36
 
37
  progress(1, desc="Query Completed")
38
- return reformulated_query, sql_query, execution_result
39
 
40
  except Exception as e:
41
  logging.error(f"Error during query generation or execution: {e}")
42
- return "Error during query processing.", "", [["No results available due to an error."]]
43
 
44
  # Function to update the schema when requested
45
  def update_schema():
@@ -54,13 +63,13 @@ def update_schema():
54
  raise gr.Error("No schema data was returned. The schema is empty.", duration=3)
55
 
56
  # Case 3: Schema successfully fetched
57
- return "Query executed successfully", gr.Info("DB Schema Updated ℹ️", duration=3)
58
-
59
 
60
  # Function to make hidden components visible after the process
61
  def continue_process():
62
- # Ensure both the SQL and result outputs are shown (since reformulated_output is always visible now)
63
- return gr.update(visible=True), gr.update(visible=True)
 
64
 
65
  # Function to reset the interface to its initial state
66
  def reset_interface():
@@ -73,21 +82,46 @@ def update_button_state(text):
73
  else:
74
  return gr.update(interactive=False)
75
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
- # Assuming 'table_names' is a list of table names fetched from the schema, e.g., ['Reports', 'Tasks', 'Clients']
 
78
 
79
- # Function to fetch table names from schema (you may already have this in your app)
80
- def get_table_names():
81
- # Replace this with actual schema fetching logic if needed
82
- return ["Reports", "Tasks", "Clients", "Agents", "Learners", "Classes", "Events", "Assesments", "Progressions", "Deliveries", "Agent Assignments", "Agent Availability", "Agent Work History"]
83
 
84
  # Function to update the query textbox when a button is clicked
85
  def insert_table_name(current_text, table_name):
86
  # Add the table name to the current text
87
  return current_text + " " + table_name
88
 
89
- # Fetch table names from schema
90
- table_names = get_table_names()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
 
93
  # Gradio interface setup
@@ -100,18 +134,23 @@ with gr.Blocks(theme=gr.themes.Soft(font=[gr.themes.GoogleFont("Ubuntu"), "Arial
100
  """)
101
  # Dynamically create buttons for each table
102
  with gr.Row():
103
- for table in table_names:
104
- gr.Button(table, size="small", elem_classes="ydcoza-small-button").click(fn=lambda current_text, t=table: insert_table_name(current_text, t), inputs=text_input, outputs=text_input)
105
-
106
- examples = gr.Examples(examples=[
107
- "I'm trying to figure out which agents are the busiest. Can you show me like the top few agents who have a lot on their plate? I'd like to see their names and maybe their contact info if we have it.",
108
- "I need to get an overview of our classes. Could you pull up a list that shows what each class is about and which client it's for? Oh, and it would be great to know how many students are in each class. Maybe order it so the biggest classes are at the top?",
109
- # "Can you give me an overview of all our classes? I'd like to see how many students are in each class and how diverse they are in terms of race. It would be helpful to see the class subject and location too. Don't leave out any classes, even if they have few or no students.",
110
- "List all classes scheduled for client EduTrust.",
111
- "I'm curious about how our classes are doing. Can you show me a list of all the classes and their latest test results or evaluations? It would be helpful to see where each class is located too.",
112
- #"I heard some agents got moved around recently. Can you find out which agents have been switched to different classes? I'd like to know where they were before, where they are now, and maybe why they were moved."
113
- "I'd like to review our agents' performance. Could you provide a summary of each agent's work history, including the classes they've taught, their roles, performance notes, and any reassignments? Highlight any instances where agents were reassigned and the reasons for the reassignment."
114
- ],example_labels=["01","02","03","04","05"], label="Demo Natural Language Queries",inputs=[text_input])
 
 
 
 
 
115
  reformulated_output = gr.Textbox(lines=2, label="Optimised Query", elem_id='ydcoza_markdown_output_desc')
116
  sql_output = gr.Code(label="Generated SQL", visible=False)
117
  sql_result_output = gr.Dataframe(label="Query Results", elem_id='result_output', visible=False) # Dataframe for SQL results
@@ -127,7 +166,12 @@ with gr.Blocks(theme=gr.themes.Soft(font=[gr.themes.GoogleFont("Ubuntu"), "Arial
127
  gr.HTML("""
128
  <span class="ydcoza_gradio_banner">View The last 50 Queries generated in Table format.</span>
129
  """)
130
- saved_queries_output = gr.Dataframe(label="Last 50 Saved Queries", headers=["Query", "Optimised Query", "SQL", "Timestamp"], interactive=True, visible=False)
 
 
 
 
 
131
  # Show the last 50 saved queries when button is clicked
132
  show_saved_queries_button = gr.Button("View Queries", elem_id='ydcoza_gradio_button')
133
  show_saved_queries_button.click(show_last_50_saved_queries, outputs=saved_queries_output).then(
@@ -146,25 +190,28 @@ with gr.Blocks(theme=gr.themes.Soft(font=[gr.themes.GoogleFont("Ubuntu"), "Arial
146
  """)
147
  # Add a button to pull the latest schema and save it to schema.json
148
  fetch_schema_button = gr.Button("Fetch Latest Schema", elem_id='ydcoza_gradio_button')
149
- fetch_schema_button.click(update_schema, outputs=[gr.Textbox(label="Schema Update Status")])
150
  fetch_schema_button.click(update_schema)
151
 
 
 
 
 
 
152
  # Setup the button click to trigger the process and show results
153
  text_input.change(fn=update_button_state, inputs=text_input, outputs=start_button)
 
154
  start_button.click(
155
  fn=query_database,
156
  inputs=[text_input],
157
- outputs=[reformulated_output, sql_output, sql_result_output]
158
  ).then(
159
  continue_process,
160
- outputs=[sql_output, sql_result_output]
161
  ).then(
162
- lambda: gr.update(interactive=False), outputs=start_button # Disable the submit button after submission
163
  )
164
 
 
165
  # Launch the Gradio interface
166
  if __name__ == "__main__":
167
- # ydcoza_face.launch(auth=auth_users, auth_message="Demo Login, Username: admin & Password: 1234")
168
- # run gradio deploy in Terminal
169
  ydcoza_face.launch()
170
-
 
1
+ import os
2
  import gradio as gr
3
  import logging
4
+ import yaml
5
+ from db import (
6
+ get_last_50_saved_queries,
7
+ initialize_local_db,
8
+ export_saved_queries_to_csv,
9
+ execute_sql_query,
10
+ fetch_and_save_schema,
11
+ show_last_50_saved_queries,
12
+ fetch_schema_info, # Now this function exists in db.py
13
+ )
14
+ from openai_integration import generate_sql_single_call # Import the updated function
15
 
16
 
17
  # Initialize logging
18
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
19
 
20
  # Call the function to ensure the table is created
21
  initialize_local_db()
 
25
  try:
26
  progress(0, desc="Starting Query Process")
27
 
28
+ # Generate SQL and reformulated query using the updated single call function
29
+ progress(0.5, desc="Generating Reformulated Query and SQL")
30
+ reformulated_query, sql_query, total_cost_per_call = generate_sql_single_call(nl_query)
31
 
32
  # Default empty result in case of SQL query failure
33
  execution_result = []
34
 
35
  # If we have a SQL query, attempt execution
36
  if sql_query and not sql_query.startswith("Error"):
37
+ progress(0.8, desc="Executing SQL Query")
38
  execution_result = execute_sql_query(sql_query)
39
 
40
  # Ensure execution_result is in a valid format for a DataFrame
 
44
  execution_result = [["No results available."]]
45
 
46
  progress(1, desc="Query Completed")
47
+ return reformulated_query, sql_query, execution_result, total_cost_per_call
48
 
49
  except Exception as e:
50
  logging.error(f"Error during query generation or execution: {e}")
51
+ return "Error during query processing.", "", [["No results available due to an error."], ""]
52
 
53
  # Function to update the schema when requested
54
  def update_schema():
 
63
  raise gr.Error("No schema data was returned. The schema is empty.", duration=3)
64
 
65
  # Case 3: Schema successfully fetched
66
+ return "Schema updated successfully", gr.Info("DB Schema Updated ℹ️", duration=3)
 
67
 
68
  # Function to make hidden components visible after the process
69
  def continue_process():
70
+ # Ensure all three outputs (SQL, result, and cost) are shown
71
+ return gr.update(visible=True), gr.update(visible=True), gr.update(visible=True)
72
+
73
 
74
  # Function to reset the interface to its initial state
75
  def reset_interface():
 
82
  else:
83
  return gr.update(interactive=False)
84
 
85
+ # Function to fetch table names from schema and format them for display
86
+ def get_table_names():
87
+ schema_info = fetch_schema_info()
88
+ if not schema_info:
89
+ return []
90
+
91
+ # Return both the original table name and the formatted name
92
+ return [
93
+ (table_name, ' '.join(word.capitalize() for word in table_name.split('_')))
94
+ for table_name in schema_info.keys()
95
+ ]
96
 
97
+ # Fetch table names as a list of tuples (original_name, formatted_name)
98
+ table_names = get_table_names()
99
 
 
 
 
 
100
 
101
  # Function to update the query textbox when a button is clicked
102
  def insert_table_name(current_text, table_name):
103
  # Add the table name to the current text
104
  return current_text + " " + table_name
105
 
106
+
107
+
108
+ # Function to load examples from YAML file
109
+ def load_examples_from_yaml(file_path):
110
+ try:
111
+ with open(file_path, 'r') as file:
112
+ examples = yaml.safe_load(file)
113
+ return examples
114
+ except Exception as e:
115
+ logging.error(f"Error loading examples: {e}")
116
+ return []
117
+
118
+ # Load examples from YAML
119
+ EXAMPLES_FILE_PATH = os.path.join(os.path.dirname(__file__), 'examples.yaml')
120
+ examples_list = load_examples_from_yaml(EXAMPLES_FILE_PATH)
121
+ # Extract the inputs for Gradio examples
122
+ example_inputs = [example['input'] for example in examples_list]
123
+ # Create numbered labels for each example (1., 2., 3., etc.)
124
+ example_labels = [f"{i+1}" for i in range(len(example_inputs))]
125
 
126
 
127
  # Gradio interface setup
 
134
  """)
135
  # Dynamically create buttons for each table
136
  with gr.Row():
137
+ # Create Gradio buttons with formatted label and insert original table name on click
138
+ for original_name, formatted_name in table_names:
139
+ gr.Button(formatted_name, size="small", elem_classes="ydcoza-small-button").click(
140
+ fn=lambda current_text, t=original_name: insert_table_name(current_text, t),
141
+ inputs=text_input,
142
+ outputs=text_input
143
+ )
144
+
145
+ # Create Gradio Examples component
146
+ examples = gr.Examples(
147
+ examples=example_inputs, # The actual inputs from the YAML file
148
+ example_labels=example_labels, # Numbered labels for buttons
149
+ label="Demo Natural Language Queries",
150
+ inputs=[text_input]
151
+ )
152
+
153
+
154
  reformulated_output = gr.Textbox(lines=2, label="Optimised Query", elem_id='ydcoza_markdown_output_desc')
155
  sql_output = gr.Code(label="Generated SQL", visible=False)
156
  sql_result_output = gr.Dataframe(label="Query Results", elem_id='result_output', visible=False) # Dataframe for SQL results
 
166
  gr.HTML("""
167
  <span class="ydcoza_gradio_banner">View The last 50 Queries generated in Table format.</span>
168
  """)
169
+ saved_queries_output = gr.Dataframe(
170
+ label="Last 50 Saved Queries",
171
+ headers=["Query", "Optimised Query", "SQL", "Timestamp"],
172
+ interactive=True,
173
+ visible=False
174
+ )
175
  # Show the last 50 saved queries when button is clicked
176
  show_saved_queries_button = gr.Button("View Queries", elem_id='ydcoza_gradio_button')
177
  show_saved_queries_button.click(show_last_50_saved_queries, outputs=saved_queries_output).then(
 
190
  """)
191
  # Add a button to pull the latest schema and save it to schema.json
192
  fetch_schema_button = gr.Button("Fetch Latest Schema", elem_id='ydcoza_gradio_button')
 
193
  fetch_schema_button.click(update_schema)
194
 
195
+ # Output for the cost information (initially hidden)
196
+ with gr.Row():
197
+ html_output_cost = gr.HTML(elem_id='ydcoza_cost_output', visible=False)
198
+
199
+
200
  # Setup the button click to trigger the process and show results
201
  text_input.change(fn=update_button_state, inputs=text_input, outputs=start_button)
202
+
203
  start_button.click(
204
  fn=query_database,
205
  inputs=[text_input],
206
+ outputs=[reformulated_output, sql_output, sql_result_output, html_output_cost] # Include the cost output here
207
  ).then(
208
  continue_process,
209
+ outputs=[sql_output, sql_result_output, html_output_cost] # Ensure cost is also shown
210
  ).then(
211
+ lambda: gr.update(interactive=False), outputs=start_button
212
  )
213
 
214
+
215
  # Launch the Gradio interface
216
  if __name__ == "__main__":
 
 
217
  ydcoza_face.launch()
 
db.py CHANGED
@@ -242,4 +242,14 @@ def reset_sqlite_db():
242
  conn.close()
243
 
244
  # Uncomment the following line to reset the SQLite database when you run this script
245
- # reset_sqlite_db()
 
 
 
 
 
 
 
 
 
 
 
242
  conn.close()
243
 
244
  # Uncomment the following line to reset the SQLite database when you run this script
245
+ # reset_sqlite_db()
246
+
247
+ def fetch_schema_info():
248
+ try:
249
+ with open("schema.json", "r") as schema_file:
250
+ schema_info = json.load(schema_file)
251
+ logging.info("Schema loaded from schema.json")
252
+ return schema_info
253
+ except Exception as e:
254
+ logging.error(f"Error loading schema from schema.json: {e}")
255
+ return {}
examples.yaml ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ - input: "I'm trying to figure out which agents are the busiest. Can you show me like the top few agents who have a lot on their plate?"
2
+ reformulated_query: "Show me the top 5 agents with the highest workload, including their names, emails, and specializations. Use Tables: agents"
3
+ sql_query: |
4
+ SELECT name, email, specialization, current_workload
5
+ FROM agents
6
+ ORDER BY current_workload DESC
7
+ LIMIT 5;
8
+
9
+ - input: "I need to get an overview of our classes. Could you pull up a list that shows what each class is about and which client it's for?"
10
+ reformulated_query: "List all classes with their subjects, client names, and the number of learners in each class, sorted by the number of learners in descending order. Use Tables: clients, learners, classes"
11
+ sql_query: |
12
+ SELECT c.id AS class_id, c.subject, cl.name AS client_name, COUNT(l.id) AS learner_count
13
+ FROM classes c
14
+ JOIN clients cl ON c.client_id = cl.id
15
+ LEFT JOIN learners l ON c.id = l.class_id
16
+ GROUP BY c.id, c.subject, cl.name
17
+ ORDER BY learner_count DESC;
18
+
19
+ - input: "Can you show me the list of agents who are available next week?"
20
+ reformulated_query: "List all agents with their names and contact information who are available next week. Use Tables: agents, agent_availability"
21
+ sql_query: |
22
+ SELECT a.name, a.email, a.phone
23
+ FROM agents a
24
+ JOIN agent_availability aa ON a.id = aa.agent_id
25
+ WHERE aa.available_date BETWEEN CURRENT_DATE + INTERVAL '7 days' AND CURRENT_DATE + INTERVAL '14 days'
26
+ AND aa.availability_status = 'available';
27
+
28
+ - input: "Which clients have classes starting next month and how many learners are enrolled in each class?"
29
+ reformulated_query: "List all clients with classes starting next month, including the client name, class subject, start date, and the number of learners enrolled in each class. Use Tables: clients, classes, learners"
30
+ sql_query: |
31
+ SELECT cl.name AS client_name, c.subject AS class_subject, c.start_date, COUNT(l.id) AS learner_count
32
+ FROM clients cl
33
+ JOIN classes c ON cl.id = c.client_id
34
+ LEFT JOIN learners l ON c.id = l.class_id
35
+ WHERE c.start_date >= date_trunc('month', CURRENT_DATE) + INTERVAL '1 month'
36
+ AND c.start_date < date_trunc('month', CURRENT_DATE) + INTERVAL '2 months'
37
+ GROUP BY cl.name, c.subject, c.start_date
38
+ ORDER BY cl.name, c.start_date;
39
+
40
+ - input: "Show me all the tasks that are overdue and assigned to 'John Doe'."
41
+ reformulated_query: "List all tasks assigned to 'John Doe' that have a due date before today and are not marked as 'completed', including the task description and due date. Use Tables: tasks"
42
+ sql_query: |
43
+ SELECT description, due_date
44
+ FROM tasks
45
+ WHERE assigned_to = 'John Doe'
46
+ AND due_date < CURRENT_DATE
47
+ AND status != 'completed';
48
+
49
+ - input: "I want to see the progression levels of learners in the history class."
50
+ reformulated_query: "List all learners in the history class, including their names and progression levels. Use Tables: learners, progressions, classes"
51
+ sql_query: |
52
+ SELECT l.name AS learner_name, p.progression_level
53
+ FROM classes c
54
+ JOIN learners l ON c.id = l.class_id
55
+ JOIN progressions p ON l.id = p.learner_id
56
+ WHERE c.subject = 'Advanced Physics';
57
+
58
+ - input: "Can you show me a list of agents who have been reassigned more than once, along with the classes they were reassigned to and the reasons?"
59
+ reformulated_query: "List all agents who have been reassigned more than once, including their names, the classes they were reassigned to, assignment dates, and the reasons for reassignment. Use Tables: agents, agent_assignments, classes"
60
+ sql_query: |
61
+ SELECT a.name AS agent_name, c.subject AS class_subject, aa.assignment_date, aa.reassignment_reason
62
+ FROM agents a
63
+ JOIN agent_assignments aa ON a.id = aa.agent_id
64
+ JOIN classes c ON aa.class_id = c.id
65
+ WHERE aa.reassigned_agent_id IS NOT NULL
66
+ AND a.id IN (
67
+ SELECT agent_id
68
+ FROM agent_assignments
69
+ WHERE reassigned_agent_id IS NOT NULL
70
+ GROUP BY agent_id
71
+ HAVING COUNT(*) > 1
72
+ )
73
+ ORDER BY a.name, aa.assignment_date;
74
+
openai_integration.py CHANGED
@@ -1,20 +1,31 @@
1
  import os
2
  import openai
3
  import json
 
4
  from dotenv import load_dotenv
5
  from db_logging import save_query_to_local_db
6
  import logging
7
-
8
- GPT_MODEL = "gpt-4o-mini"
9
-
10
- # Load environment variables from .env file
 
 
11
  load_dotenv()
 
 
 
 
 
 
12
 
13
  # Configure logging
14
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
15
 
16
- # Initialize the OpenAI client using API key from .env
17
- client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
 
 
18
 
19
  # Function to load schema from schema.json
20
  def load_schema_from_json():
@@ -41,149 +52,120 @@ def build_schema_description(schema_info):
41
  schema_description += f" - {fk['column']} references {fk['references']['table']}({fk['references']['column']})\n"
42
  return schema_description
43
 
44
- # Function to reformulate the natural language query using OpenAI API
45
- def reformulate_query(nl_query, schema_description):
46
- try:
47
- # Adding examples to guide the reformulation process
48
- examples = """
49
- Example 1:
50
- Input: "I'm trying to figure out which agents are the busiest. Can you show me like the top few agents who have a lot on their plate?"
51
- Reformulated: "Show me the top 5 agents with the highest workload, including their names, emails, and specializations. Use Tables: agents"
52
-
53
- Example 2:
54
- Input: "I need to get an overview of our classes. Could you pull up a list that shows what each class is about and which client it's for?"
55
- Reformulated: "List all classes with their subjects, client names, and the number of learners in each class, sorted by the number of learners in descending order. Use Tables: clients, learners, classes"
56
- """
57
-
58
- prompt = (
59
- f"Database Schema:\n{schema_description}\n\n"
60
- f"Using the database schema, reformulate the following natural language query to be more precise and in line with the database schema:\n{nl_query}\n\n"
61
- f"Here are some examples of how to reformulate the natural language query:\n{examples}\n\n"
62
- f"Output the query string, list the DB tables that will be used seperated by commas, don't prefix with anything and don't use markdown or quotation marks."
63
- )
64
-
65
- logging.info("Sending reformulation request to OpenAI...")
66
- response = client.chat.completions.create(
67
- model=GPT_MODEL,
68
- messages=[
69
- {"role": "system", "content": "You are an assistant that helps reformulate natural language queries and improve on them."},
70
- {"role": "user", "content": prompt}
71
- ],
72
- max_tokens=300,
73
- temperature=0.7
74
- )
75
 
76
- reformulated_query = response.choices[0].message.content.strip()
77
-
78
- logging.info(f"Reformulated Query: {reformulated_query}")
79
- return reformulated_query
 
 
 
80
  except Exception as e:
81
- logging.error(f"Error reformulating query: {e}")
82
- return str(e)
83
-
84
- # Function to generate SQL from reformulated query using OpenAI API
85
- def generate_sql_from_reformulated(reformulated_query, schema_description):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  try:
87
- # Adding examples to guide the SQL generation process
88
- examples = """
89
- Example 1:
90
- Input:
91
- "Show me the top 5 agents with the highest workload, including their names, emails, and specializations."
92
- SQL:
93
- SELECT name, email, specialization, current_workload
94
- FROM agents
95
- ORDER BY current_workload DESC
96
- LIMIT 5;
97
-
98
- Example 2:
99
- Input:
100
- "List all classes with their subjects, client names, and the number of learners in each class, sorted by the number of learners in descending order."
101
- SQL:
102
- SELECT c.id AS class_id, c.subject, cl.name AS client_name, COUNT(l.id) AS learner_count
103
- FROM classes c
104
- JOIN clients cl ON c.client_id = cl.id
105
- LEFT JOIN learners l ON c.id = l.class_id
106
- GROUP BY c.id, c.subject, cl.name
107
- ORDER BY learner_count DESC;
108
-
109
- Example 3:
110
- Input:
111
- "List all agents' work history, including the class they worked on, their role, and the start and end dates of each task."
112
- SQL:
113
- SELECT a.name AS agent_name, awh.class_id, awh.role, awh.start_date, awh.end_date
114
- FROM agents a
115
- JOIN agent_work_history awh ON a.id = awh.agent_id
116
- ORDER BY awh.start_date DESC;
117
-
118
- Example 4:
119
- Input:
120
- "Get a list of all classes with their subjects, the number of learners, and the progression status of each learner in the class."
121
- SQL:
122
- SELECT c.id AS class_id, c.subject, COUNT(l.id) AS learner_count, p.progression_level
123
- FROM classes c
124
- LEFT JOIN learners l ON c.id = l.class_id
125
- LEFT JOIN progressions p ON l.id = p.learner_id
126
- GROUP BY c.id, c.subject, p.progression_level
127
- ORDER BY learner_count DESC;
128
- """
129
 
 
 
 
 
 
130
  prompt = (
131
  f"Database Schema:\n{schema_description}\n\n"
132
- f"Natural language query:\n{reformulated_query}\n\n"
133
- f"Examples:\n{examples}\n\n"
134
- f"Convert the natural language query into an SQL query that matches the schema.\n\n"
135
- f"Use the Examples as a guide on what I expect you to do.\n\n"
136
- f"Only return the RAW SQL that can be directly executed, don't prefix it with sql, don't use quotation marks and don't use markdown."
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  )
138
-
139
- logging.info("Sending SQL generation request to OpenAI...")
140
  response = client.chat.completions.create(
141
  model=GPT_MODEL,
142
  messages=[
143
- {"role": "system", "content": "You are an assistant that converts natural language to SQL using a provided databse shema."},
 
 
 
 
 
 
 
 
 
 
 
 
144
  {"role": "user", "content": prompt}
145
  ],
146
- max_tokens=300,
147
- temperature=0.7
148
  )
149
 
150
- sql_query = response.choices[0].message.content.strip()
151
-
152
- logging.info(f"SQL Query generated: {sql_query}")
153
- return sql_query
154
- except Exception as e:
155
- logging.error(f"Error generating SQL: {e}")
156
- return str(e)
157
-
158
- # Main function to reformulate the query first and then generate SQL
159
- def generate_sql(nl_query):
160
- try:
161
- # Load the schema from schema.json
162
- schema_info = load_schema_from_json()
163
 
164
- if isinstance(schema_info, str) and schema_info.startswith("Error"):
165
- return "Error fetching schema", ""
166
 
167
- # Build the schema description once and reuse it
168
- schema_description = build_schema_description(schema_info)
169
- logging.error(f"Build Shema Description: {schema_description}")
170
 
171
- # Reformulate the query
172
- reformulated_query = reformulate_query(nl_query, schema_description)
 
 
173
 
174
- if "Error" in reformulated_query:
175
- return reformulated_query, ""
176
 
177
- # Generate SQL based on the reformulated query
178
- sql_query = generate_sql_from_reformulated(reformulated_query, schema_description)
 
 
 
 
 
 
179
 
180
- if "Error" in sql_query:
181
- return reformulated_query, sql_query
182
 
183
  # Save the query to the local database
184
  save_query_to_local_db(nl_query, reformulated_query, sql_query)
185
 
186
- return reformulated_query, sql_query
187
 
188
  except openai.error.OpenAIError as e:
189
  logging.error(f"OpenAI API error: {e}")
@@ -192,3 +174,4 @@ def generate_sql(nl_query):
192
  except Exception as e:
193
  logging.error(f"General error during SQL process: {e}")
194
  return "General error during SQL processing.", ""
 
 
1
  import os
2
  import openai
3
  import json
4
+ import yaml
5
  from dotenv import load_dotenv
6
  from db_logging import save_query_to_local_db
7
  import logging
8
+ #############LangSmith & OpenAI####################
9
+ from langsmith import traceable
10
+ from langsmith.wrappers import wrap_openai
11
+ # Initialize OpenAI client
12
+ client = wrap_openai(openai.Client())
13
+ # Load environment variables
14
  load_dotenv()
15
+ # Load the OpenAI API key
16
+ openai.api_key = os.getenv("OPENAI_API_KEY")
17
+ if not openai.api_key:
18
+ raise ValueError("Error: OPENAI_API_KEY not found in environment variables")
19
+ GPT_MODEL = "gpt-4o-mini"
20
+ ##################################################
21
 
22
  # Configure logging
23
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
24
 
25
+ # Function to calculate cost based on token usage
26
+ def ydcoza_cost(tokens_out):
27
+ t_cost = tokens_out * 0.2 / 1000000 # Rough Estimate For gpt-4o $5.00 / 1M input tokens : mini: $0.150 / 1M input tokens
28
+ return t_cost
29
 
30
  # Function to load schema from schema.json
31
  def load_schema_from_json():
 
52
  schema_description += f" - {fk['column']} references {fk['references']['table']}({fk['references']['column']})\n"
53
  return schema_description
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
+ # Function to load examples from YAML
57
+ def load_examples_from_yaml(file_path):
58
+ try:
59
+ with open(file_path, 'r') as file:
60
+ examples = yaml.safe_load(file)
61
+ logging.info("Examples loaded from examples.yaml")
62
+ return examples
63
  except Exception as e:
64
+ logging.error(f"Error loading examples: {e}")
65
+ return []
66
+
67
+ # Function to build examples string for the prompt
68
+ def build_examples_string(examples_list):
69
+ examples_string = ""
70
+ for idx, example in enumerate(examples_list, start=1):
71
+ examples_string += f"Example {idx}:\n"
72
+ examples_string += f"Input:\n{example['input']}\n"
73
+ examples_string += f"Reformulated Query:\n{example['reformulated_query']}\n"
74
+ examples_string += f"SQL Query:\n{example['sql_query']}\n\n"
75
+ return examples_string
76
+
77
+ # Load examples from YAML
78
+ EXAMPLES_FILE_PATH = os.path.join(os.path.dirname(__file__), 'examples.yaml')
79
+ examples_list = load_examples_from_yaml(EXAMPLES_FILE_PATH)
80
+ examples = build_examples_string(examples_list)
81
+ # logging.info(f"Examples From YAML: {examples}")
82
+
83
+ def generate_sql_single_call(nl_query):
84
  try:
85
+ # Load the schema from schema.json
86
+ schema_info = load_schema_from_json()
87
+
88
+ if isinstance(schema_info, str) and schema_info.startswith("Error"):
89
+ return "Error fetching schema", ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
+ # Build the schema description once and reuse it
92
+ schema_description = build_schema_description(schema_info)
93
+ logging.info(f"Schema Description: {schema_description}")
94
+
95
+ # Use the final prompt as shown above
96
  prompt = (
97
  f"Database Schema:\n{schema_description}\n\n"
98
+ f"Your task is to:\n"
99
+ f"1. Reformulate the user's natural language query to align precisely with the database schema. Also, indicate the tables to use. These tables **MUST** be present in the schema provided.\n"
100
+ f"2. Generate the corresponding SQL query based on the reformulated query and the provided schema.\n\n"
101
+ f"User's Query:\n\"{nl_query}\"\n\n"
102
+ f"Examples:\n{examples}\n"
103
+ f"Response Format (in JSON):\n"
104
+ f"{{\n"
105
+ f' "reformulated_query": "<your reformulated query>",\n'
106
+ f' "sql_query": "<your SQL query>"\n'
107
+ f"}}\n\n"
108
+ f"Important Guidelines:\n"
109
+ f"- Use only the tables and columns provided in the schema.\n"
110
+ f"- Ensure the SQL query is syntactically correct.\n"
111
+ f"- Do not include any additional text or explanations.\n"
112
+ f"- Do not include any text outside the JSON format.\n"
113
+ f"- Ensure the JSON is valid and properly formatted.\n"
114
+ f"- Avoid assumptions about data not present in the schema.\n"
115
+ f"- Double-check your response for accuracy."
116
  )
117
+ logging.info(f"Full Prompt: {prompt}")
118
+ logging.info("Sending combined request to OpenAI...")
119
  response = client.chat.completions.create(
120
  model=GPT_MODEL,
121
  messages=[
122
+ {
123
+ "role": "system",
124
+ "content": (
125
+ "You are an expert data analyst and SQL specialist. "
126
+ "Your role is to reformulate natural language queries to align with a given database schema "
127
+ # "and then generate accurate SQL queries based on the reformulated queries."
128
+ "and then create a syntactically correct PostgreSQL query to run based on the reformulated queries."
129
+ "Ensure that you only use tables and columns present in the provided schema. "
130
+ "Indicate the tables to use in your reformulated query."
131
+ "Ensure that your responses are precise, concise, and follow the provided guidelines."
132
+ "Provide your response in the specified JSON format."
133
+ )
134
+ },
135
  {"role": "user", "content": prompt}
136
  ],
137
+ max_tokens=500,
138
+ temperature=0.3 # Adjusted temperature
139
  )
140
 
141
+ # Process the assistant's response
142
+ assistant_response = response.choices[0].message.content.strip()
143
+ logging.info(f"Assistant Response:\n{assistant_response}")
 
 
 
 
 
 
 
 
 
 
144
 
 
 
145
 
 
 
 
146
 
147
+ # Calculate Tokens
148
+ tokens_used = response.usage.total_tokens
149
+ cost_per_call = ydcoza_cost(tokens_used)
150
+ total_cost_per_call = f"<p class='cost_per_call'>Tokens Consumed: {tokens_used} ; Cost per call: ${cost_per_call:.6f}</p>"
151
 
 
 
152
 
153
+ # Parse the assistant's response
154
+ try:
155
+ response_json = json.loads(assistant_response)
156
+ reformulated_query = response_json.get("reformulated_query", "")
157
+ sql_query = response_json.get("sql_query", "")
158
+ except json.JSONDecodeError:
159
+ logging.error("Could not parse assistant response as JSON.")
160
+ return "Error parsing assistant response.", ""
161
 
162
+ logging.info(f"Reformulated Query: {reformulated_query}")
163
+ logging.info(f"SQL Query generated: {sql_query}")
164
 
165
  # Save the query to the local database
166
  save_query_to_local_db(nl_query, reformulated_query, sql_query)
167
 
168
+ return reformulated_query, sql_query, total_cost_per_call
169
 
170
  except openai.error.OpenAIError as e:
171
  logging.error(f"OpenAI API error: {e}")
 
174
  except Exception as e:
175
  logging.error(f"General error during SQL process: {e}")
176
  return "General error during SQL processing.", ""
177
+
schema.json CHANGED
@@ -1,675 +1,1878 @@
1
  {
2
- "agent_assignments": {
3
- "comment": null,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  "columns": [
5
  {
6
  "name": "updated_at",
7
  "data_type": "timestamp without time zone",
8
- "comment": null
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  },
10
  {
11
- "name": "agent_id",
 
 
 
 
 
 
 
 
 
 
 
 
12
  "data_type": "integer",
13
- "comment": null
14
  },
15
  {
16
- "name": "class_id",
17
  "data_type": "integer",
18
- "comment": null
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  },
20
  {
21
- "name": "task_id",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  "data_type": "integer",
23
- "comment": null
24
  },
25
  {
26
- "name": "assignment_date",
27
- "data_type": "date",
28
- "comment": null
29
  },
30
  {
31
- "name": "reassigned_agent_id",
32
  "data_type": "integer",
33
- "comment": null
 
 
 
 
 
34
  },
35
  {
36
- "name": "reassignment_date",
37
  "data_type": "date",
38
- "comment": null
39
  },
40
  {
41
  "name": "created_at",
42
  "data_type": "timestamp without time zone",
43
- "comment": null
44
  },
45
  {
46
- "name": "id",
47
- "data_type": "integer",
48
- "comment": null
49
  },
50
  {
51
- "name": "reassignment_reason",
52
- "data_type": "text",
53
- "comment": null
54
  },
55
  {
56
- "name": "status",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  "data_type": "character varying",
58
- "comment": null
59
  }
60
  ],
61
  "foreign_keys": [
62
  {
63
- "column": "agent_id",
64
- "references": {
65
- "table": "agents",
66
- "column": "id"
67
- }
68
- },
69
- {
70
- "column": "class_id",
71
  "references": {
72
- "table": "classes",
73
- "column": "id"
74
  }
75
  },
76
  {
77
- "column": "task_id",
78
  "references": {
79
- "table": "tasks",
80
- "column": "id"
81
  }
82
  }
83
  ]
84
  },
85
- "agent_availability": {
86
- "comment": null,
87
  "columns": [
88
  {
89
  "name": "created_at",
90
  "data_type": "timestamp without time zone",
91
- "comment": null
92
  },
93
  {
94
  "name": "updated_at",
95
  "data_type": "timestamp without time zone",
96
- "comment": null
97
  },
98
  {
99
- "name": "available_date",
100
  "data_type": "date",
101
- "comment": null
102
  },
103
  {
104
- "name": "id",
105
  "data_type": "integer",
106
- "comment": null
107
  },
108
  {
109
- "name": "agent_id",
110
  "data_type": "integer",
111
- "comment": null
112
  },
113
  {
114
- "name": "availability_status",
115
- "data_type": "character varying",
116
- "comment": null
117
  },
118
  {
119
- "name": "reason",
120
- "data_type": "text",
121
- "comment": null
122
  }
123
  ],
124
  "foreign_keys": [
125
  {
126
- "column": "agent_id",
127
  "references": {
128
- "table": "agents",
129
- "column": "id"
130
  }
131
  }
132
  ]
133
  },
134
- "agent_work_history": {
135
- "comment": null,
136
  "columns": [
 
 
 
 
 
137
  {
138
  "name": "updated_at",
139
  "data_type": "timestamp without time zone",
140
- "comment": null
141
  },
142
  {
143
- "name": "agent_id",
 
 
 
 
 
144
  "data_type": "integer",
145
- "comment": null
146
  },
147
  {
148
  "name": "class_id",
149
  "data_type": "integer",
150
- "comment": null
 
 
 
 
 
151
  },
152
  {
153
- "name": "task_id",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  "data_type": "integer",
155
- "comment": null
156
  },
157
  {
158
- "name": "start_date",
159
- "data_type": "date",
160
- "comment": null
161
  },
162
  {
163
- "name": "end_date",
164
- "data_type": "date",
165
- "comment": null
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  },
167
  {
168
  "name": "created_at",
169
  "data_type": "timestamp without time zone",
170
- "comment": null
171
  },
172
  {
173
- "name": "id",
174
  "data_type": "integer",
175
- "comment": null
176
  },
177
  {
178
- "name": "reassignment_id",
179
  "data_type": "integer",
180
- "comment": null
181
  },
182
  {
183
- "name": "role",
 
 
 
 
 
 
 
 
 
 
184
  "data_type": "character varying",
185
- "comment": null
186
  },
187
  {
188
- "name": "performance_notes",
189
- "data_type": "text",
190
- "comment": null
191
  }
192
  ],
193
  "foreign_keys": [
194
  {
195
- "column": "agent_id",
196
- "references": {
197
- "table": "agents",
198
- "column": "id"
199
- }
200
- },
201
- {
202
- "column": "class_id",
203
  "references": {
204
- "table": "classes",
205
- "column": "id"
206
  }
207
  },
208
  {
209
- "column": "task_id",
210
  "references": {
211
- "table": "tasks",
212
- "column": "id"
213
  }
214
  }
215
  ]
216
  },
217
- "agents": {
218
- "comment": null,
219
  "columns": [
220
  {
221
- "name": "updated_at",
222
- "data_type": "timestamp without time zone",
223
- "comment": null
224
  },
225
  {
226
- "name": "experience",
227
  "data_type": "integer",
228
- "comment": null
229
  },
230
  {
231
- "name": "current_workload",
232
  "data_type": "integer",
233
- "comment": null
 
 
 
 
 
234
  },
235
  {
236
  "name": "created_at",
237
  "data_type": "timestamp without time zone",
238
- "comment": null
239
  },
240
  {
241
- "name": "id",
242
- "data_type": "integer",
243
- "comment": null
244
  },
245
  {
246
- "name": "location",
 
 
 
 
 
247
  "data_type": "character varying",
248
- "comment": null
249
  },
250
  {
251
- "name": "status",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  "data_type": "character varying",
253
- "comment": null
254
  },
255
  {
256
- "name": "name",
257
  "data_type": "character varying",
258
- "comment": null
259
  },
260
  {
261
- "name": "email",
262
  "data_type": "character varying",
263
- "comment": null
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  },
265
  {
266
- "name": "phone",
 
 
 
 
 
267
  "data_type": "character varying",
268
- "comment": null
269
  },
270
  {
271
- "name": "specialization",
272
  "data_type": "character varying",
273
- "comment": null
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  }
275
- ],
276
- "foreign_keys": []
277
  },
278
- "assessments": {
279
- "comment": null,
280
  "columns": [
281
  {
282
- "name": "id",
283
  "data_type": "integer",
284
- "comment": null
285
  },
286
  {
287
- "name": "class_id",
288
  "data_type": "integer",
289
- "comment": null
290
- },
291
- {
292
- "name": "assessment_date",
293
- "data_type": "date",
294
- "comment": null
295
- },
296
- {
297
- "name": "created_at",
298
- "data_type": "timestamp without time zone",
299
- "comment": null
300
  },
301
  {
302
- "name": "updated_at",
303
- "data_type": "timestamp without time zone",
304
- "comment": null
305
  },
306
  {
307
- "name": "assessment_type",
308
- "data_type": "character varying",
309
- "comment": null
310
  },
311
  {
312
- "name": "assessor_name",
313
- "data_type": "character varying",
314
- "comment": null
315
  },
316
  {
317
- "name": "result",
318
- "data_type": "character varying",
319
- "comment": null
320
  }
321
  ],
322
  "foreign_keys": [
323
  {
324
- "column": "class_id",
325
  "references": {
326
- "table": "classes",
327
- "column": "id"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
  }
329
  }
330
  ]
331
  },
332
- "classes": {
333
- "comment": null,
334
  "columns": [
335
  {
336
- "name": "id",
 
 
 
 
 
337
  "data_type": "integer",
338
- "comment": null
339
  },
340
  {
341
- "name": "client_id",
342
  "data_type": "integer",
343
- "comment": null
344
  },
345
  {
346
- "name": "start_date",
347
  "data_type": "date",
348
- "comment": null
349
  },
350
  {
351
- "name": "end_date",
352
  "data_type": "date",
353
- "comment": null
354
  },
355
  {
356
- "name": "created_at",
357
- "data_type": "timestamp without time zone",
358
- "comment": null
359
  },
360
  {
361
- "name": "updated_at",
362
- "data_type": "timestamp without time zone",
363
- "comment": null
364
  },
365
  {
366
- "name": "attendance_status",
367
- "data_type": "character varying",
368
- "comment": null
369
  },
370
  {
371
- "name": "progression_status",
372
- "data_type": "character varying",
373
- "comment": null
374
  },
375
  {
376
- "name": "subject",
377
- "data_type": "character varying",
378
- "comment": null
379
  },
380
  {
381
- "name": "site",
382
  "data_type": "character varying",
383
- "comment": null
384
  },
385
  {
386
- "name": "phase",
387
  "data_type": "character varying",
388
- "comment": null
389
  },
390
  {
391
- "name": "marketer",
392
  "data_type": "character varying",
393
- "comment": null
394
  },
395
  {
396
- "name": "status",
397
  "data_type": "character varying",
398
- "comment": null
399
- }
400
- ],
401
- "foreign_keys": [
402
- {
403
- "column": "client_id",
404
- "references": {
405
- "table": "clients",
406
- "column": "id"
407
- }
408
- }
409
- ]
410
- },
411
- "clients": {
412
- "comment": null,
413
- "columns": [
414
- {
415
- "name": "id",
416
- "data_type": "integer",
417
- "comment": null
418
  },
419
  {
420
- "name": "created_at",
421
- "data_type": "timestamp without time zone",
422
- "comment": null
423
  },
424
  {
425
- "name": "updated_at",
426
- "data_type": "timestamp without time zone",
427
- "comment": null
428
  },
429
  {
430
- "name": "email",
431
  "data_type": "character varying",
432
- "comment": null
433
  },
434
  {
435
- "name": "phone",
436
  "data_type": "character varying",
437
- "comment": null
438
  },
439
  {
440
- "name": "address",
441
- "data_type": "text",
442
- "comment": null
443
  },
444
  {
445
- "name": "status",
446
  "data_type": "character varying",
447
- "comment": null
448
  },
449
  {
450
- "name": "name",
451
  "data_type": "character varying",
452
- "comment": null
453
  },
454
  {
455
- "name": "contact_person",
456
  "data_type": "character varying",
457
- "comment": null
458
- }
459
- ],
460
- "foreign_keys": []
461
- },
462
- "deliveries": {
463
- "comment": null,
464
- "columns": [
465
- {
466
- "name": "delivery_date",
467
- "data_type": "date",
468
- "comment": null
469
  },
470
  {
471
- "name": "class_id",
472
- "data_type": "integer",
473
- "comment": null
474
  },
475
  {
476
- "name": "created_at",
477
- "data_type": "timestamp without time zone",
478
- "comment": null
479
  },
480
  {
481
- "name": "updated_at",
482
- "data_type": "timestamp without time zone",
483
- "comment": null
484
  },
485
  {
486
- "name": "id",
487
- "data_type": "integer",
488
- "comment": null
489
  },
490
  {
491
- "name": "delivery_type",
492
  "data_type": "character varying",
493
- "comment": null
494
  },
495
  {
496
- "name": "status",
497
  "data_type": "character varying",
498
- "comment": null
499
  }
500
  ],
501
  "foreign_keys": [
502
  {
503
- "column": "class_id",
504
  "references": {
505
- "table": "classes",
506
- "column": "id"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
507
  }
508
  }
509
  ]
510
  },
511
- "events": {
512
- "comment": null,
513
  "columns": [
514
  {
515
- "name": "id",
516
  "data_type": "integer",
517
- "comment": null
518
  },
519
  {
520
- "name": "created_at",
521
- "data_type": "timestamp without time zone",
522
- "comment": null
523
  },
524
  {
525
- "name": "updated_at",
526
- "data_type": "timestamp without time zone",
527
- "comment": null
528
  },
529
  {
530
- "name": "client_id",
531
- "data_type": "integer",
532
- "comment": null
533
  },
534
  {
535
- "name": "class_id",
536
- "data_type": "integer",
537
- "comment": null
538
  },
539
  {
540
- "name": "event_date",
541
- "data_type": "date",
542
- "comment": null
543
  },
544
  {
545
- "name": "reminder_date",
546
- "data_type": "date",
547
- "comment": null
548
  },
549
  {
550
- "name": "name",
551
  "data_type": "character varying",
552
- "comment": null
553
  },
554
  {
555
- "name": "event_type",
556
  "data_type": "character varying",
557
- "comment": null
558
  }
559
  ],
560
- "foreign_keys": [
 
 
 
 
561
  {
562
- "column": "class_id",
563
- "references": {
564
- "table": "classes",
565
- "column": "id"
566
- }
567
  },
568
  {
569
- "column": "client_id",
570
- "references": {
571
- "table": "clients",
572
- "column": "id"
573
- }
574
- }
575
- ]
576
- },
577
- "learners": {
578
- "comment": null,
579
- "columns": [
580
  {
581
- "name": "id",
582
  "data_type": "integer",
583
- "comment": null
584
  },
585
  {
586
- "name": "class_id",
587
  "data_type": "integer",
588
- "comment": null
589
  },
590
  {
591
  "name": "created_at",
592
  "data_type": "timestamp without time zone",
593
- "comment": null
594
  },
595
  {
596
  "name": "updated_at",
597
  "data_type": "timestamp without time zone",
598
- "comment": null
599
  },
600
  {
601
- "name": "id_number",
602
- "data_type": "character varying",
603
- "comment": null
604
  },
605
  {
606
- "name": "name",
607
  "data_type": "character varying",
608
- "comment": null
609
  },
610
  {
611
- "name": "gender",
612
- "data_type": "character varying",
613
- "comment": null
614
  },
615
  {
616
- "name": "race",
617
  "data_type": "character varying",
618
- "comment": null
 
 
 
 
 
 
 
 
 
 
619
  }
620
  ],
621
  "foreign_keys": [
622
  {
623
- "column": "class_id",
624
  "references": {
625
- "table": "classes",
626
- "column": "id"
627
  }
628
  }
629
  ]
630
  },
631
- "progressions": {
632
- "comment": null,
633
  "columns": [
634
  {
635
  "name": "updated_at",
636
  "data_type": "timestamp without time zone",
637
- "comment": null
638
  },
639
  {
640
  "name": "class_id",
641
  "data_type": "integer",
642
- "comment": null
643
  },
644
  {
645
  "name": "learner_id",
646
  "data_type": "integer",
647
- "comment": null
648
  },
649
  {
650
- "name": "created_at",
651
- "data_type": "timestamp without time zone",
652
- "comment": null
653
  },
654
  {
655
- "name": "id",
656
- "data_type": "integer",
657
- "comment": null
658
  },
659
  {
660
- "name": "completion_date",
661
  "data_type": "date",
662
- "comment": null
663
  },
664
  {
665
- "name": "progression_level",
666
- "data_type": "character varying",
667
- "comment": null
668
  },
669
  {
670
- "name": "status",
671
- "data_type": "character varying",
672
- "comment": null
 
 
 
 
 
673
  }
674
  ],
675
  "foreign_keys": [
@@ -677,126 +1880,185 @@
677
  "column": "class_id",
678
  "references": {
679
  "table": "classes",
680
- "column": "id"
681
  }
682
  },
683
  {
684
  "column": "learner_id",
685
  "references": {
686
  "table": "learners",
687
- "column": "id"
 
 
 
 
 
 
 
688
  }
689
  }
690
  ]
691
  },
692
- "reports": {
693
- "comment": null,
694
  "columns": [
695
  {
696
- "name": "related_client_id",
697
  "data_type": "integer",
698
- "comment": null
699
  },
700
  {
701
- "name": "created_at",
702
- "data_type": "timestamp without time zone",
703
- "comment": null
704
  },
705
  {
706
- "name": "updated_at",
707
- "data_type": "timestamp without time zone",
708
- "comment": null
709
  },
710
  {
711
- "name": "related_class_id",
712
- "data_type": "integer",
713
- "comment": null
714
  },
715
  {
716
- "name": "id",
717
- "data_type": "integer",
718
- "comment": null
 
 
 
 
 
719
  },
720
  {
721
- "name": "report_type",
722
  "data_type": "character varying",
723
- "comment": null
724
  },
725
  {
726
- "name": "content",
727
  "data_type": "text",
728
- "comment": null
729
  }
730
  ],
731
  "foreign_keys": [
732
  {
733
- "column": "related_class_id",
734
  "references": {
735
- "table": "classes",
736
- "column": "id"
737
  }
738
  },
739
  {
740
- "column": "related_client_id",
741
  "references": {
742
- "table": "clients",
743
- "column": "id"
744
  }
745
  }
746
  ]
747
  },
748
- "tasks": {
749
- "comment": null,
750
  "columns": [
751
  {
752
- "name": "id",
753
  "data_type": "integer",
754
- "comment": null
755
  },
756
  {
757
- "name": "event_id",
758
  "data_type": "integer",
759
- "comment": null
760
  },
761
  {
762
- "name": "due_date",
763
- "data_type": "date",
764
- "comment": null
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
765
  },
766
  {
767
  "name": "created_at",
768
  "data_type": "timestamp without time zone",
769
- "comment": null
770
  },
771
  {
772
  "name": "updated_at",
773
  "data_type": "timestamp without time zone",
774
- "comment": null
775
  },
776
  {
777
- "name": "description",
778
- "data_type": "text",
779
- "comment": null
780
  },
781
  {
782
- "name": "assigned_to",
783
  "data_type": "character varying",
784
- "comment": null
785
  },
786
  {
787
- "name": "status",
788
  "data_type": "character varying",
789
- "comment": null
790
- }
791
- ],
792
- "foreign_keys": [
793
  {
794
- "column": "event_id",
795
- "references": {
796
- "table": "events",
797
- "column": "id"
798
- }
 
 
 
 
 
 
 
 
799
  }
800
- ]
 
801
  }
802
  }
 
1
  {
2
+ "agent_absences": {
3
+ "comment": "Records instances when agents are absent from classes",
4
+ "columns": [
5
+ {
6
+ "name": "absence_id",
7
+ "data_type": "integer",
8
+ "comment": "Unique internal absence ID"
9
+ },
10
+ {
11
+ "name": "agent_id",
12
+ "data_type": "integer",
13
+ "comment": "Reference to the absent agent"
14
+ },
15
+ {
16
+ "name": "class_id",
17
+ "data_type": "integer",
18
+ "comment": "Reference to the class affected by the absence"
19
+ },
20
+ {
21
+ "name": "absence_date",
22
+ "data_type": "date",
23
+ "comment": "Date of the agent's absence"
24
+ },
25
+ {
26
+ "name": "reported_at",
27
+ "data_type": "timestamp without time zone",
28
+ "comment": "Timestamp when the absence was reported"
29
+ },
30
+ {
31
+ "name": "reason",
32
+ "data_type": "text",
33
+ "comment": "Reason for the agent's absence"
34
+ }
35
+ ],
36
+ "foreign_keys": [
37
+ {
38
+ "column": "agent_id",
39
+ "references": {
40
+ "table": "agents",
41
+ "column": "agent_id"
42
+ }
43
+ },
44
+ {
45
+ "column": "class_id",
46
+ "references": {
47
+ "table": "classes",
48
+ "column": "class_id"
49
+ }
50
+ }
51
+ ]
52
+ },
53
+ "agent_notes": {
54
+ "comment": "Stores historical notes and remarks about agents",
55
+ "columns": [
56
+ {
57
+ "name": "note_id",
58
+ "data_type": "integer",
59
+ "comment": "Unique internal note ID"
60
+ },
61
+ {
62
+ "name": "agent_id",
63
+ "data_type": "integer",
64
+ "comment": "Reference to the agent"
65
+ },
66
+ {
67
+ "name": "note_date",
68
+ "data_type": "timestamp without time zone",
69
+ "comment": "Timestamp when the note was created"
70
+ },
71
+ {
72
+ "name": "note",
73
+ "data_type": "text",
74
+ "comment": "Content of the note regarding the agent"
75
+ }
76
+ ],
77
+ "foreign_keys": [
78
+ {
79
+ "column": "agent_id",
80
+ "references": {
81
+ "table": "agents",
82
+ "column": "agent_id"
83
+ }
84
+ }
85
+ ]
86
+ },
87
+ "agent_orders": {
88
+ "comment": "Stores order information related to agents and classes",
89
+ "columns": [
90
+ {
91
+ "name": "updated_at",
92
+ "data_type": "timestamp without time zone",
93
+ "comment": "Timestamp when the order record was last updated"
94
+ },
95
+ {
96
+ "name": "agent_id",
97
+ "data_type": "integer",
98
+ "comment": "Reference to the agent"
99
+ },
100
+ {
101
+ "name": "class_id",
102
+ "data_type": "integer",
103
+ "comment": "Reference to the class"
104
+ },
105
+ {
106
+ "name": "created_at",
107
+ "data_type": "timestamp without time zone",
108
+ "comment": "Timestamp when the order record was created"
109
+ },
110
+ {
111
+ "name": "order_id",
112
+ "data_type": "integer",
113
+ "comment": "Unique internal order ID"
114
+ },
115
+ {
116
+ "name": "class_time",
117
+ "data_type": "time without time zone",
118
+ "comment": "Time when the class is scheduled"
119
+ },
120
+ {
121
+ "name": "order_hours",
122
+ "data_type": "integer",
123
+ "comment": "Number of hours linked to the agent's order for a specific class"
124
+ },
125
+ {
126
+ "name": "order_number",
127
+ "data_type": "character varying",
128
+ "comment": "Valid order number associated with the agent"
129
+ },
130
+ {
131
+ "name": "class_days",
132
+ "data_type": "character varying",
133
+ "comment": "Days when the class is scheduled"
134
+ }
135
+ ],
136
+ "foreign_keys": [
137
+ {
138
+ "column": "agent_id",
139
+ "references": {
140
+ "table": "agents",
141
+ "column": "agent_id"
142
+ }
143
+ },
144
+ {
145
+ "column": "class_id",
146
+ "references": {
147
+ "table": "classes",
148
+ "column": "class_id"
149
+ }
150
+ }
151
+ ]
152
+ },
153
+ "agent_products": {
154
+ "comment": "Associates agents with the products they are trained to teach",
155
+ "columns": [
156
+ {
157
+ "name": "agent_id",
158
+ "data_type": "integer",
159
+ "comment": "Reference to the agent"
160
+ },
161
+ {
162
+ "name": "product_id",
163
+ "data_type": "integer",
164
+ "comment": "Reference to the product the agent is trained in"
165
+ },
166
+ {
167
+ "name": "trained_start_date",
168
+ "data_type": "date",
169
+ "comment": "Start date when the agent began training in the product"
170
+ },
171
+ {
172
+ "name": "trained_end_date",
173
+ "data_type": "date",
174
+ "comment": "End date when the agent finished training in the product"
175
+ }
176
+ ],
177
+ "foreign_keys": [
178
+ {
179
+ "column": "agent_id",
180
+ "references": {
181
+ "table": "agents",
182
+ "column": "agent_id"
183
+ }
184
+ },
185
+ {
186
+ "column": "product_id",
187
+ "references": {
188
+ "table": "products",
189
+ "column": "product_id"
190
+ }
191
+ }
192
+ ]
193
+ },
194
+ "agent_qa_visits": {
195
+ "comment": "Records QA visits involving agents and classes",
196
+ "columns": [
197
+ {
198
+ "name": "visit_id",
199
+ "data_type": "integer",
200
+ "comment": "Unique internal QA visit ID"
201
+ },
202
+ {
203
+ "name": "agent_id",
204
+ "data_type": "integer",
205
+ "comment": "Reference to the agent"
206
+ },
207
+ {
208
+ "name": "class_id",
209
+ "data_type": "integer",
210
+ "comment": "Reference to the class"
211
+ },
212
+ {
213
+ "name": "visit_date",
214
+ "data_type": "date",
215
+ "comment": "Date of the QA visit"
216
+ },
217
+ {
218
+ "name": "qa_report_id",
219
+ "data_type": "integer",
220
+ "comment": "Reference to the associated QA report"
221
+ }
222
+ ],
223
+ "foreign_keys": [
224
+ {
225
+ "column": "agent_id",
226
+ "references": {
227
+ "table": "agents",
228
+ "column": "agent_id"
229
+ }
230
+ },
231
+ {
232
+ "column": "class_id",
233
+ "references": {
234
+ "table": "classes",
235
+ "column": "class_id"
236
+ }
237
+ },
238
+ {
239
+ "column": "qa_report_id",
240
+ "references": {
241
+ "table": "qa_reports",
242
+ "column": "qa_report_id"
243
+ }
244
+ }
245
+ ]
246
+ },
247
+ "agent_replacements": {
248
+ "comment": "Records instances of agent replacements in classes",
249
+ "columns": [
250
+ {
251
+ "name": "replacement_id",
252
+ "data_type": "integer",
253
+ "comment": "Unique internal replacement ID"
254
+ },
255
+ {
256
+ "name": "class_id",
257
+ "data_type": "integer",
258
+ "comment": "Reference to the class"
259
+ },
260
+ {
261
+ "name": "original_agent_id",
262
+ "data_type": "integer",
263
+ "comment": "Reference to the original agent"
264
+ },
265
+ {
266
+ "name": "replacement_agent_id",
267
+ "data_type": "integer",
268
+ "comment": "Reference to the replacement agent"
269
+ },
270
+ {
271
+ "name": "start_date",
272
+ "data_type": "date",
273
+ "comment": "Date when the replacement starts"
274
+ },
275
+ {
276
+ "name": "end_date",
277
+ "data_type": "date",
278
+ "comment": "Date when the replacement ends"
279
+ },
280
+ {
281
+ "name": "reason",
282
+ "data_type": "text",
283
+ "comment": "Reason for the agent's replacement"
284
+ }
285
+ ],
286
+ "foreign_keys": [
287
+ {
288
+ "column": "class_id",
289
+ "references": {
290
+ "table": "classes",
291
+ "column": "class_id"
292
+ }
293
+ },
294
+ {
295
+ "column": "original_agent_id",
296
+ "references": {
297
+ "table": "agents",
298
+ "column": "agent_id"
299
+ }
300
+ },
301
+ {
302
+ "column": "replacement_agent_id",
303
+ "references": {
304
+ "table": "agents",
305
+ "column": "agent_id"
306
+ }
307
+ }
308
+ ]
309
+ },
310
+ "agents": {
311
+ "comment": "Stores information about agents (instructors or facilitators)",
312
+ "columns": [
313
+ {
314
+ "name": "agent_id",
315
+ "data_type": "integer",
316
+ "comment": "Unique internal agent ID"
317
+ },
318
+ {
319
+ "name": "residential_town_id",
320
+ "data_type": "integer",
321
+ "comment": "Reference to the town where the agent lives"
322
+ },
323
+ {
324
+ "name": "preferred_working_area_1",
325
+ "data_type": "integer",
326
+ "comment": "Agent's first preferred working area"
327
+ },
328
+ {
329
+ "name": "preferred_working_area_2",
330
+ "data_type": "integer",
331
+ "comment": "Agent's second preferred working area"
332
+ },
333
+ {
334
+ "name": "preferred_working_area_3",
335
+ "data_type": "integer",
336
+ "comment": "Agent's third preferred working area"
337
+ },
338
+ {
339
+ "name": "sace_registration_date",
340
+ "data_type": "date",
341
+ "comment": "Date when the agent's SACE registration became effective"
342
+ },
343
+ {
344
+ "name": "sace_expiry_date",
345
+ "data_type": "date",
346
+ "comment": "Expiry date of the agent's provisional SACE registration"
347
+ },
348
+ {
349
+ "name": "date_loaded",
350
+ "data_type": "date",
351
+ "comment": "Date when the agent was added to the system"
352
+ },
353
+ {
354
+ "name": "quantum_result_communications",
355
+ "data_type": "numeric",
356
+ "comment": "Agent's competence score in Communications (percentage)"
357
+ },
358
+ {
359
+ "name": "quantum_result_mathematics",
360
+ "data_type": "numeric",
361
+ "comment": "Agent's competence score in Mathematics (percentage)"
362
+ },
363
+ {
364
+ "name": "quantum_result_training",
365
+ "data_type": "numeric",
366
+ "comment": "Agent's competence score in Training/Facilitating (percentage)"
367
+ },
368
+ {
369
+ "name": "agent_training_date",
370
+ "data_type": "date",
371
+ "comment": "Date when the agent received induction training"
372
+ },
373
+ {
374
+ "name": "signed_agreement",
375
+ "data_type": "boolean",
376
+ "comment": "Indicates if the agent has a signed agreement (true) or not (false)"
377
+ },
378
+ {
379
+ "name": "signed_agreement_date",
380
+ "data_type": "date",
381
+ "comment": "Date when the agent signed the agreement"
382
+ },
383
+ {
384
+ "name": "created_at",
385
+ "data_type": "timestamp without time zone",
386
+ "comment": "Timestamp when the agent record was created"
387
+ },
388
+ {
389
+ "name": "updated_at",
390
+ "data_type": "timestamp without time zone",
391
+ "comment": "Timestamp when the agent record was last updated"
392
+ },
393
+ {
394
+ "name": "bank_name",
395
+ "data_type": "character varying",
396
+ "comment": "Name of the agent's bank"
397
+ },
398
+ {
399
+ "name": "highest_qualification",
400
+ "data_type": "character varying",
401
+ "comment": "Highest qualification the agent has achieved"
402
+ },
403
+ {
404
+ "name": "sace_registration_number",
405
+ "data_type": "character varying",
406
+ "comment": "Agent's SACE (South African Council for Educators) registration number"
407
+ },
408
+ {
409
+ "name": "first_name",
410
+ "data_type": "character varying",
411
+ "comment": "Agent's first name"
412
+ },
413
+ {
414
+ "name": "initials",
415
+ "data_type": "character varying",
416
+ "comment": "Agent's initials"
417
+ },
418
+ {
419
+ "name": "surname",
420
+ "data_type": "character varying",
421
+ "comment": "Agent's surname"
422
+ },
423
+ {
424
+ "name": "gender",
425
+ "data_type": "character varying",
426
+ "comment": "Agent's gender"
427
+ },
428
+ {
429
+ "name": "race",
430
+ "data_type": "character varying",
431
+ "comment": "Agent's race; options include 'African', 'Coloured', 'White', 'Indian'"
432
+ },
433
+ {
434
+ "name": "sa_id_no",
435
+ "data_type": "character varying",
436
+ "comment": "Agent's South African ID number"
437
+ },
438
+ {
439
+ "name": "passport_number",
440
+ "data_type": "character varying",
441
+ "comment": "Agent's passport number if they are a foreigner"
442
+ },
443
+ {
444
+ "name": "tel_number",
445
+ "data_type": "character varying",
446
+ "comment": "Agent's primary telephone number"
447
+ },
448
+ {
449
+ "name": "email_address",
450
+ "data_type": "character varying",
451
+ "comment": "Agent's email address"
452
+ },
453
+ {
454
+ "name": "residential_address_line",
455
+ "data_type": "character varying",
456
+ "comment": "Agent's residential street address"
457
+ },
458
+ {
459
+ "name": "residential_suburb",
460
+ "data_type": "character varying",
461
+ "comment": "Agent's residential suburb"
462
+ },
463
+ {
464
+ "name": "bank_branch_code",
465
+ "data_type": "character varying",
466
+ "comment": "Branch code of the agent's bank"
467
+ },
468
+ {
469
+ "name": "residential_postal_code",
470
+ "data_type": "character varying",
471
+ "comment": "Postal code of the agent's residential area"
472
+ },
473
+ {
474
+ "name": "bank_account_number",
475
+ "data_type": "character varying",
476
+ "comment": "Agent's bank account number"
477
+ },
478
+ {
479
+ "name": "agent_notes",
480
+ "data_type": "text",
481
+ "comment": "Notes regarding the agent's performance, issues, or other relevant information"
482
+ }
483
+ ],
484
+ "foreign_keys": [
485
+ {
486
+ "column": "preferred_working_area_1",
487
+ "references": {
488
+ "table": "locations",
489
+ "column": "location_id"
490
+ }
491
+ },
492
+ {
493
+ "column": "preferred_working_area_2",
494
+ "references": {
495
+ "table": "locations",
496
+ "column": "location_id"
497
+ }
498
+ },
499
+ {
500
+ "column": "preferred_working_area_3",
501
+ "references": {
502
+ "table": "locations",
503
+ "column": "location_id"
504
+ }
505
+ },
506
+ {
507
+ "column": "residential_town_id",
508
+ "references": {
509
+ "table": "locations",
510
+ "column": "location_id"
511
+ }
512
+ }
513
+ ]
514
+ },
515
+ "attendance_records": {
516
+ "comment": "Associates learners with their attendance status on specific dates",
517
+ "columns": [
518
+ {
519
+ "name": "register_id",
520
+ "data_type": "integer",
521
+ "comment": "Reference to the attendance register"
522
+ },
523
+ {
524
+ "name": "learner_id",
525
+ "data_type": "integer",
526
+ "comment": "Reference to the learner"
527
+ },
528
+ {
529
+ "name": "status",
530
+ "data_type": "character varying",
531
+ "comment": "Attendance status of the learner (e.g., 'Present', 'Absent')"
532
+ }
533
+ ],
534
+ "foreign_keys": [
535
+ {
536
+ "column": "learner_id",
537
+ "references": {
538
+ "table": "learners",
539
+ "column": "learner_id"
540
+ }
541
+ },
542
+ {
543
+ "column": "register_id",
544
+ "references": {
545
+ "table": "attendance_registers",
546
+ "column": "register_id"
547
+ }
548
+ }
549
+ ]
550
+ },
551
+ "attendance_registers": {
552
+ "comment": "Records attendance registers for classes",
553
+ "columns": [
554
+ {
555
+ "name": "register_id",
556
+ "data_type": "integer",
557
+ "comment": "Unique internal attendance register ID"
558
+ },
559
+ {
560
+ "name": "class_id",
561
+ "data_type": "integer",
562
+ "comment": "Reference to the class"
563
+ },
564
+ {
565
+ "name": "date",
566
+ "data_type": "date",
567
+ "comment": "Date of the attendance"
568
+ },
569
+ {
570
+ "name": "agent_id",
571
+ "data_type": "integer",
572
+ "comment": "Reference to the agent who conducted the attendance"
573
+ },
574
+ {
575
+ "name": "created_at",
576
+ "data_type": "timestamp without time zone",
577
+ "comment": "Timestamp when the attendance register was created"
578
+ },
579
+ {
580
+ "name": "updated_at",
581
+ "data_type": "timestamp without time zone",
582
+ "comment": "Timestamp when the attendance register was last updated"
583
+ }
584
+ ],
585
+ "foreign_keys": [
586
+ {
587
+ "column": "agent_id",
588
+ "references": {
589
+ "table": "agents",
590
+ "column": "agent_id"
591
+ }
592
+ },
593
+ {
594
+ "column": "class_id",
595
+ "references": {
596
+ "table": "classes",
597
+ "column": "class_id"
598
+ }
599
+ }
600
+ ]
601
+ },
602
+ "class_agents": {
603
+ "comment": "Associates agents with classes they facilitate, including their roles and durations",
604
+ "columns": [
605
+ {
606
+ "name": "class_id",
607
+ "data_type": "integer",
608
+ "comment": "Reference to the class"
609
+ },
610
+ {
611
+ "name": "agent_id",
612
+ "data_type": "integer",
613
+ "comment": "Reference to the agent facilitating the class"
614
+ },
615
+ {
616
+ "name": "start_date",
617
+ "data_type": "date",
618
+ "comment": "Date when the agent started facilitating the class"
619
+ },
620
+ {
621
+ "name": "end_date",
622
+ "data_type": "date",
623
+ "comment": "Date when the agent stopped facilitating the class"
624
+ },
625
+ {
626
+ "name": "role",
627
+ "data_type": "character varying",
628
+ "comment": "Role of the agent in the class (e.g., 'Original', 'Backup', 'Replacement')"
629
+ }
630
+ ],
631
+ "foreign_keys": [
632
+ {
633
+ "column": "agent_id",
634
+ "references": {
635
+ "table": "agents",
636
+ "column": "agent_id"
637
+ }
638
+ },
639
+ {
640
+ "column": "class_id",
641
+ "references": {
642
+ "table": "classes",
643
+ "column": "class_id"
644
+ }
645
+ }
646
+ ]
647
+ },
648
+ "class_notes": {
649
+ "comment": "Stores historical notes and remarks about classes",
650
+ "columns": [
651
+ {
652
+ "name": "note_id",
653
+ "data_type": "integer",
654
+ "comment": "Unique internal note ID"
655
+ },
656
+ {
657
+ "name": "class_id",
658
+ "data_type": "integer",
659
+ "comment": "Reference to the class"
660
+ },
661
+ {
662
+ "name": "note_date",
663
+ "data_type": "timestamp without time zone",
664
+ "comment": "Timestamp when the note was created"
665
+ },
666
+ {
667
+ "name": "note",
668
+ "data_type": "text",
669
+ "comment": "Content of the note regarding the class"
670
+ }
671
+ ],
672
+ "foreign_keys": [
673
+ {
674
+ "column": "class_id",
675
+ "references": {
676
+ "table": "classes",
677
+ "column": "class_id"
678
+ }
679
+ }
680
+ ]
681
+ },
682
+ "class_schedules": {
683
+ "comment": "Stores scheduling information for classes",
684
+ "columns": [
685
+ {
686
+ "name": "schedule_id",
687
+ "data_type": "integer",
688
+ "comment": "Unique internal schedule ID"
689
+ },
690
+ {
691
+ "name": "class_id",
692
+ "data_type": "integer",
693
+ "comment": "Reference to the class"
694
+ },
695
+ {
696
+ "name": "start_time",
697
+ "data_type": "time without time zone",
698
+ "comment": "Class start time"
699
+ },
700
+ {
701
+ "name": "end_time",
702
+ "data_type": "time without time zone",
703
+ "comment": "Class end time"
704
+ },
705
+ {
706
+ "name": "day_of_week",
707
+ "data_type": "character varying",
708
+ "comment": "Day of the week when the class occurs (e.g., 'Monday')"
709
+ }
710
+ ],
711
+ "foreign_keys": [
712
+ {
713
+ "column": "class_id",
714
+ "references": {
715
+ "table": "classes",
716
+ "column": "class_id"
717
+ }
718
+ }
719
+ ]
720
+ },
721
+ "class_subjects": {
722
+ "comment": "Associates classes with the subjects or products being taught",
723
+ "columns": [
724
+ {
725
+ "name": "class_id",
726
+ "data_type": "integer",
727
+ "comment": "Reference to the class"
728
+ },
729
+ {
730
+ "name": "product_id",
731
+ "data_type": "integer",
732
+ "comment": "Reference to the subject or product taught in the class"
733
+ }
734
+ ],
735
+ "foreign_keys": [
736
+ {
737
+ "column": "class_id",
738
+ "references": {
739
+ "table": "classes",
740
+ "column": "class_id"
741
+ }
742
+ },
743
+ {
744
+ "column": "product_id",
745
+ "references": {
746
+ "table": "products",
747
+ "column": "product_id"
748
+ }
749
+ }
750
+ ]
751
+ },
752
+ "classes": {
753
+ "comment": "Stores information about classes, including scheduling and associations",
754
  "columns": [
755
  {
756
  "name": "updated_at",
757
  "data_type": "timestamp without time zone",
758
+ "comment": "Timestamp when the class record was last updated"
759
+ },
760
+ {
761
+ "name": "client_id",
762
+ "data_type": "integer",
763
+ "comment": "Reference to the client associated with the class"
764
+ },
765
+ {
766
+ "name": "stop_date",
767
+ "data_type": "date",
768
+ "comment": "Date when the class stopped"
769
+ },
770
+ {
771
+ "name": "restart_date",
772
+ "data_type": "date",
773
+ "comment": "Date when the class restarted"
774
+ },
775
+ {
776
+ "name": "seta_funded",
777
+ "data_type": "boolean",
778
+ "comment": "Indicates if the project is SETA funded (true) or not (false)"
779
+ },
780
+ {
781
+ "name": "exam_class",
782
+ "data_type": "boolean",
783
+ "comment": "Indicates if this is an exam project (true) or not (false)"
784
+ },
785
+ {
786
+ "name": "project_supervisor_id",
787
+ "data_type": "integer",
788
+ "comment": "Reference to the project supervisor managing the class"
789
+ },
790
+ {
791
+ "name": "delivery_date",
792
+ "data_type": "date",
793
+ "comment": "Date when materials or resources must be delivered to the class"
794
+ },
795
+ {
796
+ "name": "collection_date",
797
+ "data_type": "date",
798
+ "comment": "Date when portfolios or materials must be collected from the class"
799
+ },
800
+ {
801
+ "name": "created_at",
802
+ "data_type": "timestamp without time zone",
803
+ "comment": "Timestamp when the class record was created"
804
+ },
805
+ {
806
+ "name": "class_id",
807
+ "data_type": "integer",
808
+ "comment": "Unique internal class ID"
809
+ },
810
+ {
811
+ "name": "class_town_id",
812
+ "data_type": "integer",
813
+ "comment": "Reference to the town where the class takes place"
814
+ },
815
+ {
816
+ "name": "original_start_date",
817
+ "data_type": "date",
818
+ "comment": "Original start date of the class"
819
+ },
820
+ {
821
+ "name": "class_site_name",
822
+ "data_type": "character varying",
823
+ "comment": "Name of the class or site"
824
+ },
825
+ {
826
+ "name": "class_address_line",
827
+ "data_type": "character varying",
828
+ "comment": "Street address where the class takes place"
829
+ },
830
+ {
831
+ "name": "class_suburb",
832
+ "data_type": "character varying",
833
+ "comment": "Suburb where the class takes place"
834
+ },
835
+ {
836
+ "name": "class_notes",
837
+ "data_type": "text",
838
+ "comment": "Notes about the class; important information to remember"
839
+ },
840
+ {
841
+ "name": "class_postal_code",
842
+ "data_type": "character varying",
843
+ "comment": "Postal code of the class location"
844
+ },
845
+ {
846
+ "name": "class_type",
847
+ "data_type": "character varying",
848
+ "comment": "Type of class; determines the 'rules' (e.g., 'Employed', 'Community')"
849
+ },
850
+ {
851
+ "name": "class_status",
852
+ "data_type": "character varying",
853
+ "comment": "Status of the class (e.g., 'New', 'Restarted', 'Stopped')"
854
+ },
855
+ {
856
+ "name": "exam_type",
857
+ "data_type": "character varying",
858
+ "comment": "Type of exam associated with the class"
859
+ },
860
+ {
861
+ "name": "seta",
862
+ "data_type": "character varying",
863
+ "comment": "Name of the SETA (Sector Education and Training Authority) the client belongs to"
864
+ }
865
+ ],
866
+ "foreign_keys": [
867
+ {
868
+ "column": "class_town_id",
869
+ "references": {
870
+ "table": "locations",
871
+ "column": "location_id"
872
+ }
873
+ },
874
+ {
875
+ "column": "client_id",
876
+ "references": {
877
+ "table": "clients",
878
+ "column": "client_id"
879
+ }
880
+ },
881
+ {
882
+ "column": "project_supervisor_id",
883
+ "references": {
884
+ "table": "users",
885
+ "column": "user_id"
886
+ }
887
+ }
888
+ ]
889
+ },
890
+ "client_communications": {
891
+ "comment": "Stores records of communications with clients",
892
+ "columns": [
893
+ {
894
+ "name": "client_id",
895
+ "data_type": "integer",
896
+ "comment": "Reference to the client"
897
+ },
898
+ {
899
+ "name": "communication_date",
900
+ "data_type": "timestamp without time zone",
901
+ "comment": "Date and time when the communication occurred"
902
+ },
903
+ {
904
+ "name": "user_id",
905
+ "data_type": "integer",
906
+ "comment": "Reference to the user who communicated with the client"
907
+ },
908
+ {
909
+ "name": "communication_id",
910
+ "data_type": "integer",
911
+ "comment": "Unique internal communication ID"
912
+ },
913
+ {
914
+ "name": "subject",
915
+ "data_type": "character varying",
916
+ "comment": "Subject of the communication"
917
+ },
918
+ {
919
+ "name": "communication_type",
920
+ "data_type": "character varying",
921
+ "comment": "Type of communication (e.g., 'Email', 'Phone Call')"
922
+ },
923
+ {
924
+ "name": "content",
925
+ "data_type": "text",
926
+ "comment": "Content or summary of the communication"
927
+ }
928
+ ],
929
+ "foreign_keys": [
930
+ {
931
+ "column": "client_id",
932
+ "references": {
933
+ "table": "clients",
934
+ "column": "client_id"
935
+ }
936
  },
937
  {
938
+ "column": "user_id",
939
+ "references": {
940
+ "table": "users",
941
+ "column": "user_id"
942
+ }
943
+ }
944
+ ]
945
+ },
946
+ "client_contact_persons": {
947
+ "comment": "Stores contact person information for clients",
948
+ "columns": [
949
+ {
950
+ "name": "contact_id",
951
  "data_type": "integer",
952
+ "comment": "Unique internal contact person ID"
953
  },
954
  {
955
+ "name": "client_id",
956
  "data_type": "integer",
957
+ "comment": "Reference to the client"
958
+ },
959
+ {
960
+ "name": "first_name",
961
+ "data_type": "character varying",
962
+ "comment": "First name of the contact person"
963
+ },
964
+ {
965
+ "name": "surname",
966
+ "data_type": "character varying",
967
+ "comment": "Surname of the contact person"
968
+ },
969
+ {
970
+ "name": "email",
971
+ "data_type": "character varying",
972
+ "comment": "Email address of the contact person"
973
+ },
974
+ {
975
+ "name": "cellphone_number",
976
+ "data_type": "character varying",
977
+ "comment": "Cellphone number of the contact person"
978
+ },
979
+ {
980
+ "name": "tel_number",
981
+ "data_type": "character varying",
982
+ "comment": "Landline number of the contact person"
983
  },
984
  {
985
+ "name": "position",
986
+ "data_type": "character varying",
987
+ "comment": "Position or role of the contact person at the client company"
988
+ }
989
+ ],
990
+ "foreign_keys": [
991
+ {
992
+ "column": "client_id",
993
+ "references": {
994
+ "table": "clients",
995
+ "column": "client_id"
996
+ }
997
+ }
998
+ ]
999
+ },
1000
+ "clients": {
1001
+ "comment": "Stores information about clients (companies or organizations)",
1002
+ "columns": [
1003
+ {
1004
+ "name": "client_id",
1005
  "data_type": "integer",
1006
+ "comment": "Unique internal client ID"
1007
  },
1008
  {
1009
+ "name": "branch_of",
1010
+ "data_type": "integer",
1011
+ "comment": "Reference to the parent client if this client is a branch"
1012
  },
1013
  {
1014
+ "name": "town_id",
1015
  "data_type": "integer",
1016
+ "comment": "Reference to the town where the client is located"
1017
+ },
1018
+ {
1019
+ "name": "financial_year_end",
1020
+ "data_type": "date",
1021
+ "comment": "Date of the client's financial year-end"
1022
  },
1023
  {
1024
+ "name": "bbbee_verification_date",
1025
  "data_type": "date",
1026
+ "comment": "Date of the client's BBBEE verification"
1027
  },
1028
  {
1029
  "name": "created_at",
1030
  "data_type": "timestamp without time zone",
1031
+ "comment": "Timestamp when the client record was created"
1032
  },
1033
  {
1034
+ "name": "updated_at",
1035
+ "data_type": "timestamp without time zone",
1036
+ "comment": "Timestamp when the client record was last updated"
1037
  },
1038
  {
1039
+ "name": "postal_code",
1040
+ "data_type": "character varying",
1041
+ "comment": "Postal code of the client's location"
1042
  },
1043
  {
1044
+ "name": "client_name",
1045
+ "data_type": "character varying",
1046
+ "comment": "Name of the client company or organization"
1047
+ },
1048
+ {
1049
+ "name": "seta",
1050
+ "data_type": "character varying",
1051
+ "comment": "SETA the client belongs to"
1052
+ },
1053
+ {
1054
+ "name": "company_registration_number",
1055
+ "data_type": "character varying",
1056
+ "comment": "Company registration number of the client"
1057
+ },
1058
+ {
1059
+ "name": "address_line",
1060
+ "data_type": "character varying",
1061
+ "comment": "Client's street address"
1062
+ },
1063
+ {
1064
+ "name": "suburb",
1065
+ "data_type": "character varying",
1066
+ "comment": "Suburb where the client is located"
1067
+ },
1068
+ {
1069
+ "name": "client_status",
1070
  "data_type": "character varying",
1071
+ "comment": "Current status of the client (e.g., 'Active Client', 'Lost Client')"
1072
  }
1073
  ],
1074
  "foreign_keys": [
1075
  {
1076
+ "column": "branch_of",
 
 
 
 
 
 
 
1077
  "references": {
1078
+ "table": "clients",
1079
+ "column": "client_id"
1080
  }
1081
  },
1082
  {
1083
+ "column": "town_id",
1084
  "references": {
1085
+ "table": "locations",
1086
+ "column": "location_id"
1087
  }
1088
  }
1089
  ]
1090
  },
1091
+ "collections": {
1092
+ "comment": "Records collections made from classes",
1093
  "columns": [
1094
  {
1095
  "name": "created_at",
1096
  "data_type": "timestamp without time zone",
1097
+ "comment": "Timestamp when the collection record was created"
1098
  },
1099
  {
1100
  "name": "updated_at",
1101
  "data_type": "timestamp without time zone",
1102
+ "comment": "Timestamp when the collection record was last updated"
1103
  },
1104
  {
1105
+ "name": "collection_date",
1106
  "data_type": "date",
1107
+ "comment": "Date when the collection is scheduled or occurred"
1108
  },
1109
  {
1110
+ "name": "collection_id",
1111
  "data_type": "integer",
1112
+ "comment": "Unique internal collection ID"
1113
  },
1114
  {
1115
+ "name": "class_id",
1116
  "data_type": "integer",
1117
+ "comment": "Reference to the class"
1118
  },
1119
  {
1120
+ "name": "items",
1121
+ "data_type": "text",
1122
+ "comment": "Items collected from the class"
1123
  },
1124
  {
1125
+ "name": "status",
1126
+ "data_type": "character varying",
1127
+ "comment": "Collection status (e.g., 'Pending', 'Collected')"
1128
  }
1129
  ],
1130
  "foreign_keys": [
1131
  {
1132
+ "column": "class_id",
1133
  "references": {
1134
+ "table": "classes",
1135
+ "column": "class_id"
1136
  }
1137
  }
1138
  ]
1139
  },
1140
+ "deliveries": {
1141
+ "comment": "Records deliveries made to classes",
1142
  "columns": [
1143
+ {
1144
+ "name": "created_at",
1145
+ "data_type": "timestamp without time zone",
1146
+ "comment": "Timestamp when the delivery record was created"
1147
+ },
1148
  {
1149
  "name": "updated_at",
1150
  "data_type": "timestamp without time zone",
1151
+ "comment": "Timestamp when the delivery record was last updated"
1152
  },
1153
  {
1154
+ "name": "delivery_date",
1155
+ "data_type": "date",
1156
+ "comment": "Date when the delivery is scheduled or occurred"
1157
+ },
1158
+ {
1159
+ "name": "delivery_id",
1160
  "data_type": "integer",
1161
+ "comment": "Unique internal delivery ID"
1162
  },
1163
  {
1164
  "name": "class_id",
1165
  "data_type": "integer",
1166
+ "comment": "Reference to the class"
1167
+ },
1168
+ {
1169
+ "name": "items",
1170
+ "data_type": "text",
1171
+ "comment": "Items included in the delivery"
1172
  },
1173
  {
1174
+ "name": "status",
1175
+ "data_type": "character varying",
1176
+ "comment": "Delivery status (e.g., 'Pending', 'Delivered')"
1177
+ }
1178
+ ],
1179
+ "foreign_keys": [
1180
+ {
1181
+ "column": "class_id",
1182
+ "references": {
1183
+ "table": "classes",
1184
+ "column": "class_id"
1185
+ }
1186
+ }
1187
+ ]
1188
+ },
1189
+ "employers": {
1190
+ "comment": "Stores information about employers or sponsors of learners",
1191
+ "columns": [
1192
+ {
1193
+ "name": "employer_id",
1194
  "data_type": "integer",
1195
+ "comment": "Unique internal employer ID"
1196
  },
1197
  {
1198
+ "name": "created_at",
1199
+ "data_type": "timestamp without time zone",
1200
+ "comment": "Timestamp when the employer record was created"
1201
  },
1202
  {
1203
+ "name": "updated_at",
1204
+ "data_type": "timestamp without time zone",
1205
+ "comment": "Timestamp when the employer record was last updated"
1206
+ },
1207
+ {
1208
+ "name": "employer_name",
1209
+ "data_type": "character varying",
1210
+ "comment": "Name of the employer or sponsoring organization"
1211
+ }
1212
+ ],
1213
+ "foreign_keys": []
1214
+ },
1215
+ "exam_results": {
1216
+ "comment": "Stores detailed exam results for learners",
1217
+ "columns": [
1218
+ {
1219
+ "name": "updated_at",
1220
+ "data_type": "timestamp without time zone",
1221
+ "comment": "Timestamp when the exam result was last updated"
1222
+ },
1223
+ {
1224
+ "name": "exam_id",
1225
+ "data_type": "integer",
1226
+ "comment": "Reference to the exam"
1227
+ },
1228
+ {
1229
+ "name": "learner_id",
1230
+ "data_type": "integer",
1231
+ "comment": "Reference to the learner"
1232
  },
1233
  {
1234
  "name": "created_at",
1235
  "data_type": "timestamp without time zone",
1236
+ "comment": "Timestamp when the exam result was created"
1237
  },
1238
  {
1239
+ "name": "result_id",
1240
  "data_type": "integer",
1241
+ "comment": "Unique internal exam result ID"
1242
  },
1243
  {
1244
+ "name": "mock_exam_number",
1245
  "data_type": "integer",
1246
+ "comment": "Number of the mock exam (e.g., 1, 2, 3)"
1247
  },
1248
  {
1249
+ "name": "score",
1250
+ "data_type": "numeric",
1251
+ "comment": "Learner's score in the exam"
1252
+ },
1253
+ {
1254
+ "name": "exam_date",
1255
+ "data_type": "date",
1256
+ "comment": "Date when the exam was taken"
1257
+ },
1258
+ {
1259
+ "name": "subject",
1260
  "data_type": "character varying",
1261
+ "comment": "Subject of the exam"
1262
  },
1263
  {
1264
+ "name": "result",
1265
+ "data_type": "character varying",
1266
+ "comment": "Exam result (e.g., 'Pass', 'Fail')"
1267
  }
1268
  ],
1269
  "foreign_keys": [
1270
  {
1271
+ "column": "exam_id",
 
 
 
 
 
 
 
1272
  "references": {
1273
+ "table": "exams",
1274
+ "column": "exam_id"
1275
  }
1276
  },
1277
  {
1278
+ "column": "learner_id",
1279
  "references": {
1280
+ "table": "learners",
1281
+ "column": "learner_id"
1282
  }
1283
  }
1284
  ]
1285
  },
1286
+ "exams": {
1287
+ "comment": "Stores exam results for learners",
1288
  "columns": [
1289
  {
1290
+ "name": "exam_id",
1291
+ "data_type": "integer",
1292
+ "comment": "Unique internal exam ID"
1293
  },
1294
  {
1295
+ "name": "learner_id",
1296
  "data_type": "integer",
1297
+ "comment": "Reference to the learner"
1298
  },
1299
  {
1300
+ "name": "product_id",
1301
  "data_type": "integer",
1302
+ "comment": "Reference to the product or subject"
1303
+ },
1304
+ {
1305
+ "name": "exam_date",
1306
+ "data_type": "date",
1307
+ "comment": "Date when the exam was taken"
1308
  },
1309
  {
1310
  "name": "created_at",
1311
  "data_type": "timestamp without time zone",
1312
+ "comment": "Timestamp when the exam record was created"
1313
  },
1314
  {
1315
+ "name": "updated_at",
1316
+ "data_type": "timestamp without time zone",
1317
+ "comment": "Timestamp when the exam record was last updated"
1318
  },
1319
  {
1320
+ "name": "score",
1321
+ "data_type": "numeric",
1322
+ "comment": "Learner's score in the exam"
1323
+ },
1324
+ {
1325
+ "name": "exam_type",
1326
  "data_type": "character varying",
1327
+ "comment": "Type of exam (e.g., 'Mock', 'Final')"
1328
  },
1329
  {
1330
+ "name": "result",
1331
+ "data_type": "character varying",
1332
+ "comment": "Exam result (e.g., 'Pass', 'Fail')"
1333
+ }
1334
+ ],
1335
+ "foreign_keys": [
1336
+ {
1337
+ "column": "learner_id",
1338
+ "references": {
1339
+ "table": "learners",
1340
+ "column": "learner_id"
1341
+ }
1342
+ },
1343
+ {
1344
+ "column": "product_id",
1345
+ "references": {
1346
+ "table": "products",
1347
+ "column": "product_id"
1348
+ }
1349
+ }
1350
+ ]
1351
+ },
1352
+ "files": {
1353
+ "comment": "Stores references to files associated with various entities",
1354
+ "columns": [
1355
+ {
1356
+ "name": "file_id",
1357
+ "data_type": "integer",
1358
+ "comment": "Unique internal file ID"
1359
+ },
1360
+ {
1361
+ "name": "owner_id",
1362
+ "data_type": "integer",
1363
+ "comment": "ID of the owner entity"
1364
+ },
1365
+ {
1366
+ "name": "uploaded_at",
1367
+ "data_type": "timestamp without time zone",
1368
+ "comment": "Timestamp when the file was uploaded"
1369
+ },
1370
+ {
1371
+ "name": "owner_type",
1372
  "data_type": "character varying",
1373
+ "comment": "Type of entity that owns the file (e.g., 'Learner', 'Class', 'Agent')"
1374
  },
1375
  {
1376
+ "name": "file_path",
1377
  "data_type": "character varying",
1378
+ "comment": "File path or URL to the stored file"
1379
  },
1380
  {
1381
+ "name": "file_type",
1382
  "data_type": "character varying",
1383
+ "comment": "Type of file (e.g., 'Scanned Portfolio', 'QA Report')"
1384
+ }
1385
+ ],
1386
+ "foreign_keys": []
1387
+ },
1388
+ "history": {
1389
+ "comment": "Records historical changes and actions performed on entities",
1390
+ "columns": [
1391
+ {
1392
+ "name": "action_date",
1393
+ "data_type": "timestamp without time zone",
1394
+ "comment": "Timestamp when the action occurred"
1395
+ },
1396
+ {
1397
+ "name": "user_id",
1398
+ "data_type": "integer",
1399
+ "comment": "Reference to the user who performed the action"
1400
+ },
1401
+ {
1402
+ "name": "entity_id",
1403
+ "data_type": "integer",
1404
+ "comment": "ID of the entity"
1405
+ },
1406
+ {
1407
+ "name": "history_id",
1408
+ "data_type": "integer",
1409
+ "comment": "Unique internal history ID"
1410
  },
1411
  {
1412
+ "name": "changes",
1413
+ "data_type": "jsonb",
1414
+ "comment": "Details of the changes made, stored in JSON format"
1415
+ },
1416
+ {
1417
+ "name": "action",
1418
  "data_type": "character varying",
1419
+ "comment": "Type of action performed (e.g., 'Created', 'Updated', 'Deleted')"
1420
  },
1421
  {
1422
+ "name": "entity_type",
1423
  "data_type": "character varying",
1424
+ "comment": "Type of entity the history record refers to (e.g., 'Learner', 'Agent', 'Class')"
1425
+ }
1426
+ ],
1427
+ "foreign_keys": [
1428
+ {
1429
+ "column": "user_id",
1430
+ "references": {
1431
+ "table": "users",
1432
+ "column": "user_id"
1433
+ }
1434
+ }
1435
+ ]
1436
+ },
1437
+ "learner_products": {
1438
+ "comment": "Associates learners with the products they are enrolled in",
1439
+ "columns": [
1440
+ {
1441
+ "name": "learner_id",
1442
+ "data_type": "integer",
1443
+ "comment": "Reference to the learner"
1444
+ },
1445
+ {
1446
+ "name": "product_id",
1447
+ "data_type": "integer",
1448
+ "comment": "Reference to the product the learner is enrolled in"
1449
+ },
1450
+ {
1451
+ "name": "start_date",
1452
+ "data_type": "date",
1453
+ "comment": "Start date of the learner's enrollment in the product"
1454
+ },
1455
+ {
1456
+ "name": "end_date",
1457
+ "data_type": "date",
1458
+ "comment": "End date of the learner's enrollment in the product"
1459
+ }
1460
+ ],
1461
+ "foreign_keys": [
1462
+ {
1463
+ "column": "learner_id",
1464
+ "references": {
1465
+ "table": "learners",
1466
+ "column": "learner_id"
1467
+ }
1468
+ },
1469
+ {
1470
+ "column": "product_id",
1471
+ "references": {
1472
+ "table": "products",
1473
+ "column": "product_id"
1474
+ }
1475
  }
1476
+ ]
 
1477
  },
1478
+ "learner_progressions": {
1479
+ "comment": "Tracks the progression of learners between products",
1480
  "columns": [
1481
  {
1482
+ "name": "progression_id",
1483
  "data_type": "integer",
1484
+ "comment": "Unique internal progression ID"
1485
  },
1486
  {
1487
+ "name": "learner_id",
1488
  "data_type": "integer",
1489
+ "comment": "Reference to the learner"
 
 
 
 
 
 
 
 
 
 
1490
  },
1491
  {
1492
+ "name": "from_product_id",
1493
+ "data_type": "integer",
1494
+ "comment": "Reference to the initial product"
1495
  },
1496
  {
1497
+ "name": "to_product_id",
1498
+ "data_type": "integer",
1499
+ "comment": "Reference to the new product after progression"
1500
  },
1501
  {
1502
+ "name": "progression_date",
1503
+ "data_type": "date",
1504
+ "comment": "Date when the learner progressed to the new product"
1505
  },
1506
  {
1507
+ "name": "notes",
1508
+ "data_type": "text",
1509
+ "comment": "Additional notes regarding the progression"
1510
  }
1511
  ],
1512
  "foreign_keys": [
1513
  {
1514
+ "column": "from_product_id",
1515
  "references": {
1516
+ "table": "products",
1517
+ "column": "product_id"
1518
+ }
1519
+ },
1520
+ {
1521
+ "column": "learner_id",
1522
+ "references": {
1523
+ "table": "learners",
1524
+ "column": "learner_id"
1525
+ }
1526
+ },
1527
+ {
1528
+ "column": "to_product_id",
1529
+ "references": {
1530
+ "table": "products",
1531
+ "column": "product_id"
1532
  }
1533
  }
1534
  ]
1535
  },
1536
+ "learners": {
1537
+ "comment": "Stores personal, educational, and assessment information about learners",
1538
  "columns": [
1539
  {
1540
+ "name": "updated_at",
1541
+ "data_type": "timestamp without time zone",
1542
+ "comment": "Timestamp when the learner record was last updated"
1543
+ },
1544
+ {
1545
+ "name": "city_town_id",
1546
  "data_type": "integer",
1547
+ "comment": "Reference to the city or town where the learner lives"
1548
  },
1549
  {
1550
+ "name": "province_region_id",
1551
  "data_type": "integer",
1552
+ "comment": "Reference to the province/region where the learner lives"
1553
  },
1554
  {
1555
+ "name": "date_loaded",
1556
  "data_type": "date",
1557
+ "comment": "Date when the learner was added to the system"
1558
  },
1559
  {
1560
+ "name": "placement_assessment_date",
1561
  "data_type": "date",
1562
+ "comment": "Date when the learner took the placement assessment"
1563
  },
1564
  {
1565
+ "name": "employment_status",
1566
+ "data_type": "boolean",
1567
+ "comment": "Indicates if the learner is employed (true) or not (false)"
1568
  },
1569
  {
1570
+ "name": "employer_id",
1571
+ "data_type": "integer",
1572
+ "comment": "Reference to the learner's employer or sponsor"
1573
  },
1574
  {
1575
+ "name": "disability_status",
1576
+ "data_type": "boolean",
1577
+ "comment": "Indicates if the learner has a disability (true) or not (false)"
1578
  },
1579
  {
1580
+ "name": "created_at",
1581
+ "data_type": "timestamp without time zone",
1582
+ "comment": "Timestamp when the learner record was created"
1583
  },
1584
  {
1585
+ "name": "learner_id",
1586
+ "data_type": "integer",
1587
+ "comment": "Unique internal learner ID"
1588
  },
1589
  {
1590
+ "name": "email_address",
1591
  "data_type": "character varying",
1592
+ "comment": "Learner's email address"
1593
  },
1594
  {
1595
+ "name": "address_line_1",
1596
  "data_type": "character varying",
1597
+ "comment": "First line of learner's physical address"
1598
  },
1599
  {
1600
+ "name": "address_line_2",
1601
  "data_type": "character varying",
1602
+ "comment": "Second line of learner's physical address"
1603
  },
1604
  {
1605
+ "name": "scanned_portfolio",
1606
  "data_type": "character varying",
1607
+ "comment": "File path or URL to the learner's scanned portfolio in PDF format"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1608
  },
1609
  {
1610
+ "name": "placement_level_communications",
1611
+ "data_type": "character varying",
1612
+ "comment": "Learner's initial placement level in Communications (e.g., 'CL1b', 'CL1', 'CL2')"
1613
  },
1614
  {
1615
+ "name": "postal_code",
1616
+ "data_type": "character varying",
1617
+ "comment": "Postal code of the learner's area"
1618
  },
1619
  {
1620
+ "name": "highest_qualification",
1621
  "data_type": "character varying",
1622
+ "comment": "Highest qualification the learner has achieved"
1623
  },
1624
  {
1625
+ "name": "placement_level_mathematics",
1626
  "data_type": "character varying",
1627
+ "comment": "Learner's initial placement level in Mathematics (e.g., 'ML1', 'ML2', 'ML3')"
1628
  },
1629
  {
1630
+ "name": "assessment_status",
1631
+ "data_type": "character varying",
1632
+ "comment": "Assessment status; indicates if the learner was assessed ('Assessed', 'Not Assessed')"
1633
  },
1634
  {
1635
+ "name": "first_name",
1636
  "data_type": "character varying",
1637
+ "comment": "Learner's first name"
1638
  },
1639
  {
1640
+ "name": "initials",
1641
  "data_type": "character varying",
1642
+ "comment": "Learner's initials"
1643
  },
1644
  {
1645
+ "name": "surname",
1646
  "data_type": "character varying",
1647
+ "comment": "Learner's surname"
 
 
 
 
 
 
 
 
 
 
 
1648
  },
1649
  {
1650
+ "name": "gender",
1651
+ "data_type": "character varying",
1652
+ "comment": "Learner's gender"
1653
  },
1654
  {
1655
+ "name": "race",
1656
+ "data_type": "character varying",
1657
+ "comment": "Learner's race; options include 'African', 'Coloured', 'White', 'Indian'"
1658
  },
1659
  {
1660
+ "name": "sa_id_no",
1661
+ "data_type": "character varying",
1662
+ "comment": "Learner's South African ID number"
1663
  },
1664
  {
1665
+ "name": "passport_number",
1666
+ "data_type": "character varying",
1667
+ "comment": "Learner's passport number if they are a foreigner"
1668
  },
1669
  {
1670
+ "name": "tel_number",
1671
  "data_type": "character varying",
1672
+ "comment": "Learner's primary telephone number"
1673
  },
1674
  {
1675
+ "name": "alternative_tel_number",
1676
  "data_type": "character varying",
1677
+ "comment": "Learner's alternative contact number"
1678
  }
1679
  ],
1680
  "foreign_keys": [
1681
  {
1682
+ "column": "city_town_id",
1683
  "references": {
1684
+ "table": "locations",
1685
+ "column": "location_id"
1686
+ }
1687
+ },
1688
+ {
1689
+ "column": "employer_id",
1690
+ "references": {
1691
+ "table": "employers",
1692
+ "column": "employer_id"
1693
+ }
1694
+ },
1695
+ {
1696
+ "column": "province_region_id",
1697
+ "references": {
1698
+ "table": "locations",
1699
+ "column": "location_id"
1700
  }
1701
  }
1702
  ]
1703
  },
1704
+ "locations": {
1705
+ "comment": "Stores geographical location data for addresses",
1706
  "columns": [
1707
  {
1708
+ "name": "location_id",
1709
  "data_type": "integer",
1710
+ "comment": "Unique internal location ID"
1711
  },
1712
  {
1713
+ "name": "longitude",
1714
+ "data_type": "numeric",
1715
+ "comment": "Geographical longitude coordinate"
1716
  },
1717
  {
1718
+ "name": "latitude",
1719
+ "data_type": "numeric",
1720
+ "comment": "Geographical latitude coordinate"
1721
  },
1722
  {
1723
+ "name": "created_at",
1724
+ "data_type": "timestamp without time zone",
1725
+ "comment": "Timestamp when the location record was created"
1726
  },
1727
  {
1728
+ "name": "updated_at",
1729
+ "data_type": "timestamp without time zone",
1730
+ "comment": "Timestamp when the location record was last updated"
1731
  },
1732
  {
1733
+ "name": "suburb",
1734
+ "data_type": "character varying",
1735
+ "comment": "Suburb name"
1736
  },
1737
  {
1738
+ "name": "town",
1739
+ "data_type": "character varying",
1740
+ "comment": "Town name"
1741
  },
1742
  {
1743
+ "name": "province",
1744
  "data_type": "character varying",
1745
+ "comment": "Province name"
1746
  },
1747
  {
1748
+ "name": "postal_code",
1749
  "data_type": "character varying",
1750
+ "comment": "Postal code"
1751
  }
1752
  ],
1753
+ "foreign_keys": []
1754
+ },
1755
+ "products": {
1756
+ "comment": "Stores information about educational products or courses",
1757
+ "columns": [
1758
  {
1759
+ "name": "product_id",
1760
+ "data_type": "integer",
1761
+ "comment": "Unique internal product ID"
 
 
1762
  },
1763
  {
1764
+ "name": "product_duration",
1765
+ "data_type": "integer",
1766
+ "comment": "Total duration of the product in hours"
1767
+ },
 
 
 
 
 
 
 
1768
  {
1769
+ "name": "learning_area_duration",
1770
  "data_type": "integer",
1771
+ "comment": "Duration of each learning area in hours"
1772
  },
1773
  {
1774
+ "name": "parent_product_id",
1775
  "data_type": "integer",
1776
+ "comment": "Reference to a parent product for hierarchical structuring"
1777
  },
1778
  {
1779
  "name": "created_at",
1780
  "data_type": "timestamp without time zone",
1781
+ "comment": "Timestamp when the product record was created"
1782
  },
1783
  {
1784
  "name": "updated_at",
1785
  "data_type": "timestamp without time zone",
1786
+ "comment": "Timestamp when the product record was last updated"
1787
  },
1788
  {
1789
+ "name": "product_notes",
1790
+ "data_type": "text",
1791
+ "comment": "Notes or additional information about the product"
1792
  },
1793
  {
1794
+ "name": "product_name",
1795
  "data_type": "character varying",
1796
+ "comment": "Name of the product or course"
1797
  },
1798
  {
1799
+ "name": "product_rules",
1800
+ "data_type": "text",
1801
+ "comment": "Rules or guidelines associated with the product"
1802
  },
1803
  {
1804
+ "name": "learning_area",
1805
  "data_type": "character varying",
1806
+ "comment": "Learning areas covered by the product (e.g., 'Communication', 'Numeracy')"
1807
+ },
1808
+ {
1809
+ "name": "product_flags",
1810
+ "data_type": "text",
1811
+ "comment": "Flags or alerts for the product (e.g., attendance thresholds)"
1812
+ },
1813
+ {
1814
+ "name": "reporting_structure",
1815
+ "data_type": "text",
1816
+ "comment": "Structure of progress reports for the product"
1817
  }
1818
  ],
1819
  "foreign_keys": [
1820
  {
1821
+ "column": "parent_product_id",
1822
  "references": {
1823
+ "table": "products",
1824
+ "column": "product_id"
1825
  }
1826
  }
1827
  ]
1828
  },
1829
+ "progress_reports": {
1830
+ "comment": "Stores progress reports for learners in specific classes and products",
1831
  "columns": [
1832
  {
1833
  "name": "updated_at",
1834
  "data_type": "timestamp without time zone",
1835
+ "comment": "Timestamp when the progress report was last updated"
1836
  },
1837
  {
1838
  "name": "class_id",
1839
  "data_type": "integer",
1840
+ "comment": "Reference to the class"
1841
  },
1842
  {
1843
  "name": "learner_id",
1844
  "data_type": "integer",
1845
+ "comment": "Reference to the learner"
1846
  },
1847
  {
1848
+ "name": "product_id",
1849
+ "data_type": "integer",
1850
+ "comment": "Reference to the product or subject"
1851
  },
1852
  {
1853
+ "name": "progress_percentage",
1854
+ "data_type": "numeric",
1855
+ "comment": "Learner's progress percentage in the product"
1856
  },
1857
  {
1858
+ "name": "report_date",
1859
  "data_type": "date",
1860
+ "comment": "Date when the progress report was generated"
1861
  },
1862
  {
1863
+ "name": "report_id",
1864
+ "data_type": "integer",
1865
+ "comment": "Unique internal progress report ID"
1866
  },
1867
  {
1868
+ "name": "created_at",
1869
+ "data_type": "timestamp without time zone",
1870
+ "comment": "Timestamp when the progress report was created"
1871
+ },
1872
+ {
1873
+ "name": "remarks",
1874
+ "data_type": "text",
1875
+ "comment": "Additional remarks or comments"
1876
  }
1877
  ],
1878
  "foreign_keys": [
 
1880
  "column": "class_id",
1881
  "references": {
1882
  "table": "classes",
1883
+ "column": "class_id"
1884
  }
1885
  },
1886
  {
1887
  "column": "learner_id",
1888
  "references": {
1889
  "table": "learners",
1890
+ "column": "learner_id"
1891
+ }
1892
+ },
1893
+ {
1894
+ "column": "product_id",
1895
+ "references": {
1896
+ "table": "products",
1897
+ "column": "product_id"
1898
  }
1899
  }
1900
  ]
1901
  },
1902
+ "qa_reports": {
1903
+ "comment": "Stores QA (Quality Assurance) reports for classes and agents",
1904
  "columns": [
1905
  {
1906
+ "name": "qa_report_id",
1907
  "data_type": "integer",
1908
+ "comment": "Unique internal QA report ID"
1909
  },
1910
  {
1911
+ "name": "class_id",
1912
+ "data_type": "integer",
1913
+ "comment": "Reference to the class"
1914
  },
1915
  {
1916
+ "name": "agent_id",
1917
+ "data_type": "integer",
1918
+ "comment": "Reference to the agent"
1919
  },
1920
  {
1921
+ "name": "report_date",
1922
+ "data_type": "date",
1923
+ "comment": "Date when the QA report was created"
1924
  },
1925
  {
1926
+ "name": "created_at",
1927
+ "data_type": "timestamp without time zone",
1928
+ "comment": "Timestamp when the QA report was created"
1929
+ },
1930
+ {
1931
+ "name": "updated_at",
1932
+ "data_type": "timestamp without time zone",
1933
+ "comment": "Timestamp when the QA report was last updated"
1934
  },
1935
  {
1936
+ "name": "report_file",
1937
  "data_type": "character varying",
1938
+ "comment": "File path or URL to the QA report"
1939
  },
1940
  {
1941
+ "name": "notes",
1942
  "data_type": "text",
1943
+ "comment": "Additional notes or observations from the QA report"
1944
  }
1945
  ],
1946
  "foreign_keys": [
1947
  {
1948
+ "column": "agent_id",
1949
  "references": {
1950
+ "table": "agents",
1951
+ "column": "agent_id"
1952
  }
1953
  },
1954
  {
1955
+ "column": "class_id",
1956
  "references": {
1957
+ "table": "classes",
1958
+ "column": "class_id"
1959
  }
1960
  }
1961
  ]
1962
  },
1963
+ "user_permissions": {
1964
+ "comment": "Grants specific permissions to users",
1965
  "columns": [
1966
  {
1967
+ "name": "permission_id",
1968
  "data_type": "integer",
1969
+ "comment": "Unique internal permission ID"
1970
  },
1971
  {
1972
+ "name": "user_id",
1973
  "data_type": "integer",
1974
+ "comment": "Reference to the user"
1975
  },
1976
  {
1977
+ "name": "permission",
1978
+ "data_type": "character varying",
1979
+ "comment": "Specific permission granted to the user"
1980
+ }
1981
+ ],
1982
+ "foreign_keys": [
1983
+ {
1984
+ "column": "user_id",
1985
+ "references": {
1986
+ "table": "users",
1987
+ "column": "user_id"
1988
+ }
1989
+ }
1990
+ ]
1991
+ },
1992
+ "user_roles": {
1993
+ "comment": "Defines roles and associated permissions for users",
1994
+ "columns": [
1995
+ {
1996
+ "name": "role_id",
1997
+ "data_type": "integer",
1998
+ "comment": "Unique internal role ID"
1999
+ },
2000
+ {
2001
+ "name": "permissions",
2002
+ "data_type": "jsonb",
2003
+ "comment": "Permissions associated with the role, stored in JSON format"
2004
+ },
2005
+ {
2006
+ "name": "role_name",
2007
+ "data_type": "character varying",
2008
+ "comment": "Name of the role (e.g., 'Admin', 'Project Supervisor')"
2009
+ }
2010
+ ],
2011
+ "foreign_keys": []
2012
+ },
2013
+ "users": {
2014
+ "comment": "Stores system user information",
2015
+ "columns": [
2016
+ {
2017
+ "name": "user_id",
2018
+ "data_type": "integer",
2019
+ "comment": "Unique internal user ID"
2020
  },
2021
  {
2022
  "name": "created_at",
2023
  "data_type": "timestamp without time zone",
2024
+ "comment": "Timestamp when the user record was created"
2025
  },
2026
  {
2027
  "name": "updated_at",
2028
  "data_type": "timestamp without time zone",
2029
+ "comment": "Timestamp when the user record was last updated"
2030
  },
2031
  {
2032
+ "name": "email",
2033
+ "data_type": "character varying",
2034
+ "comment": "User's email address"
2035
  },
2036
  {
2037
+ "name": "cellphone_number",
2038
  "data_type": "character varying",
2039
+ "comment": "User's cellphone number"
2040
  },
2041
  {
2042
+ "name": "role",
2043
  "data_type": "character varying",
2044
+ "comment": "User's role in the system, e.g., 'Admin', 'Project Supervisor'"
2045
+ },
 
 
2046
  {
2047
+ "name": "password_hash",
2048
+ "data_type": "character varying",
2049
+ "comment": "Hashed password for user authentication"
2050
+ },
2051
+ {
2052
+ "name": "first_name",
2053
+ "data_type": "character varying",
2054
+ "comment": "User's first name"
2055
+ },
2056
+ {
2057
+ "name": "surname",
2058
+ "data_type": "character varying",
2059
+ "comment": "User's surname"
2060
  }
2061
+ ],
2062
+ "foreign_keys": []
2063
  }
2064
  }