Skip to content

Commit

Permalink
Updates to time columns and timestamp columns
Browse files Browse the repository at this point in the history
  • Loading branch information
brendannee committed Nov 14, 2024
1 parent 8a1e125 commit 6c2665b
Show file tree
Hide file tree
Showing 13 changed files with 102 additions and 165 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Updated
- Renamed time caching functions
- Updates to time columns and timestamp columns

### Fixed
- Better GTFS export for currency
Expand Down
13 changes: 1 addition & 12 deletions src/lib/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,18 +94,7 @@ export const exportGtfs = async (initialConfig: Config) => {
}

if (model.filenameExtension === 'txt') {
const excludeColumns = [
'arrival_timestamp',
'departure_timestamp',
'start_timestamp',
'end_timestamp',
'service_arrival_timestamp',
'service_departure_timestamp',
'boarding_timestamp',
'alighting_timestamp',
'ridership_start_timestamp',
'ridership_end_timestamp',
];
const excludeColumns = [];

// If no routes have values for agency_id, add it to the excludeColumns list
if (model.filenameBase === 'routes') {
Expand Down
122 changes: 70 additions & 52 deletions src/lib/import-gtfs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ import { updateGtfsRealtimeData } from './import-gtfs-realtime.ts';
import { log, logError, logWarning } from './log-utils.ts';
import {
calculateSecondsFromMidnight,
getTimestampColumnName,
padLeadingZeros,
setDefaultConfig,
validateConfigForImport,
padLeadingZeros,
} from './utils.ts';

import { Config, ConfigAgency, Model } from '../types/global_interfaces.ts';
Expand Down Expand Up @@ -81,21 +82,6 @@ const getTextFiles = async (folderPath: string): Promise<string[]> => {
return files.filter((filename) => filename.slice(-3) === 'txt');
};

const TIME_COLUMN_NAMES = [
'start_time',
'end_time',
'arrival_time',
'departure_time',
'prior_notice_last_time',
'prior_notice_start_time',
'start_pickup_drop_off_window',
];

const TIME_COLUMN_PAIRS = TIME_COLUMN_NAMES.map((name) => [
name,
name.endsWith('time') ? `${name}stamp` : `${name}_timestamp`,
]);

const downloadGtfsFiles = async (task: GtfsImportTask): Promise<void> => {
if (!task.url) {
throw new Error('No `url` specified in config');
Expand Down Expand Up @@ -196,7 +182,9 @@ const createGtfsTables = (db: Database.Database): void => {
return;
}

const columns = model.schema.map((column) => {
const sqlColumnCreateStatements = [];

for (const column of model.schema) {
const checks = [];
if (column.min !== undefined && column.max) {
checks.push(
Expand All @@ -212,9 +200,7 @@ const createGtfsTables = (db: Database.Database): void => {
checks.push(
`(TYPEOF(${column.name}) = 'integer' OR ${column.name} IS NULL)`,
);
}

if (column.type === 'real') {
} else if (column.type === 'real') {
checks.push(
`(TYPEOF(${column.name}) = 'real' OR ${column.name} IS NULL)`,
);
Expand All @@ -225,24 +211,32 @@ const createGtfsTables = (db: Database.Database): void => {
const columnCollation = column.nocase ? 'COLLATE NOCASE' : '';
const checkClause =
checks.length > 0 ? `CHECK(${checks.join(' AND ')})` : '';
return `${column.name} ${column.type} ${checkClause} ${required} ${columnDefault} ${columnCollation}`;
});

sqlColumnCreateStatements.push(
`${column.name} ${column.type} ${checkClause} ${required} ${columnDefault} ${columnCollation}`,
);

// Add an additional timestamp column for time columns
if (column.type === 'time') {
sqlColumnCreateStatements.push(
`${getTimestampColumnName(column.name)} INTEGER`,
);
}
}

// Find Primary Key fields
const primaryColumns = model.schema.filter((column) => column.primary);

if (primaryColumns.length > 0) {
columns.push(
`PRIMARY KEY (${primaryColumns
.map((column) => column.name)
.join(', ')})`,
sqlColumnCreateStatements.push(
`PRIMARY KEY (${primaryColumns.map(({ name }) => name).join(', ')})`,
);
}

db.prepare(`DROP TABLE IF EXISTS ${model.filenameBase};`).run();

db.prepare(
`CREATE TABLE ${model.filenameBase} (${columns.join(', ')});`,
`CREATE TABLE ${model.filenameBase} (${sqlColumnCreateStatements.join(', ')});`,
).run();
}
};
Expand All @@ -252,10 +246,20 @@ const createGtfsIndexes = (db: Database.Database): void => {
if (!model.schema) {
return;
}
for (const column of model.schema.filter((column) => column.index)) {
db.prepare(
`CREATE INDEX idx_${model.filenameBase}_${column.name} ON ${model.filenameBase} (${column.name});`,
).run();
for (const column of model.schema) {
if (column.index) {
db.prepare(
`CREATE INDEX idx_${model.filenameBase}_${column.name} ON ${model.filenameBase} (${column.name});`,
).run();
}

if (column.type === 'time') {
// Index all timestamp columns
const timestampColumnName = getTimestampColumnName(column.name);
db.prepare(
`CREATE INDEX idx_${model.filenameBase}_${timestampColumnName} ON ${model.filenameBase} (${timestampColumnName});`,
).run();
}
}
}
};
Expand All @@ -272,13 +276,17 @@ const formatGtfsLine = (
const filenameBase = model.filenameBase;
const filenameExtension = model.filenameExtension;

for (const columnSchema of model.schema) {
const { name, type, required, min, max } = columnSchema;
for (const { name, type, required } of model.schema) {
let value = line[name];

// Early null check
if (value === '' || value === undefined || value === null) {
formattedLine[name] = null;

if (type === 'time') {
formattedLine[getTimestampColumnName(name)] = null;
}

if (required) {
throw new Error(
`Missing required value in ${filenameBase}.${filenameExtension} for ${name} on line ${lineNumber}.`,
Expand All @@ -295,20 +303,18 @@ const formatGtfsLine = (
`Invalid date in ${filenameBase}.${filenameExtension} for ${name} on line ${lineNumber}.`,
);
}
}

formattedLine[name] = value;
}

// Process time columns
for (const [timeColumnName, timestampColumnName] of TIME_COLUMN_PAIRS) {
const value = formattedLine[timeColumnName];
if (value) {
} else if (type === 'time') {
// Add an additional timestamp column for time columns
const [timeAsSecondsFromMidnight, timeAsString] =
formatAndCacheTime(value);
formattedLine[timestampColumnName] = timeAsSecondsFromMidnight;
formattedLine[timeColumnName] = timeAsString;

value = timeAsString;

formattedLine[getTimestampColumnName(name)] =
timeAsSecondsFromMidnight ?? null;
}

formattedLine[name] = value;
}

return formattedLine;
Expand Down Expand Up @@ -354,22 +360,34 @@ const importGtfsFiles = (

task.log(`Importing - ${filename}\r`);

const placeholder = model.schema
.map(({ name }) => `@${name}`)
.join(', ');
// Create a list of all columns
const columns = model.schema.flatMap((column) => {
if (column.type === 'time') {
// Add an additional timestamp column for time columns
return [
column,
{
name: getTimestampColumnName(column.name),
type: 'integer',
index: true,
},
];
}
return column;
});

// Create a map of which columns need prefixing
const prefixedColumns = new Set(
model.schema
columns
.filter((column) => column.prefix)
.map((column) => column.name),
);

const prepareStatement = `INSERT ${task.ignoreDuplicates ? 'OR IGNORE' : ''} INTO ${
model.filenameBase
} (${model.schema
.map((column) => column.name)
.join(', ')}) VALUES (${placeholder})`;
} (${columns.map(({ name }) => name).join(', ')}) VALUES (${columns
.map(({ name }) => `@${name}`)
.join(', ')})`;

const insert = db.prepare(prepareStatement);

Expand All @@ -393,7 +411,7 @@ const importGtfsFiles = (
}
} catch (error: any) {
if (error.code === 'SQLITE_CONSTRAINT_PRIMARYKEY') {
const primaryColumns = model.schema.filter(
const primaryColumns = columns.filter(
(column) => column.primary,
);
task.logWarning(
Expand Down
11 changes: 11 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,3 +324,14 @@ export function formatCurrency(value: number, currency: string) {

return `${integerPart}${fractionPart !== '' ? `.${fractionPart}` : ''}`;
}

/**
* Gets the timestamp column name for a given column name
* @param columnName The column name
* @returns The timestamp column name
*/
export function getTimestampColumnName(columnName: string) {
return columnName.endsWith('time')
? `${columnName}stamp`
: `${columnName}_timestamp`;
}
14 changes: 2 additions & 12 deletions src/models/gtfs-ride/board-alight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,21 +104,11 @@ export const boardAlight = {
},
{
name: 'service_arrival_time',
type: 'text',
},
{
name: 'service_arrival_timestamp',
type: 'integer',
index: true,
type: 'time',
},
{
name: 'service_departure_time',
type: 'text',
},
{
name: 'service_departure_timestamp',
type: 'integer',
index: true,
type: 'time',
},
{
name: 'source',
Expand Down
14 changes: 2 additions & 12 deletions src/models/gtfs-ride/rider-trip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,11 @@ export const riderTrip = {
},
{
name: 'boarding_time',
type: 'text',
},
{
name: 'boarding_timestamp',
type: 'integer',
index: true,
type: 'time',
},
{
name: 'alighting_time',
type: 'text',
},
{
name: 'alighting_timestamp',
type: 'integer',
index: true,
type: 'time',
},
{
name: 'rider_type',
Expand Down
14 changes: 2 additions & 12 deletions src/models/gtfs-ride/ridership.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,11 @@ export const ridership = {
},
{
name: 'ridership_start_time',
type: 'text',
},
{
name: 'ridership_start_timestamp',
type: 'integer',
index: true,
type: 'time',
},
{
name: 'ridership_end_time',
type: 'text',
},
{
name: 'ridership_end_timestamp',
type: 'integer',
index: true,
type: 'time',
},
{
name: 'service_id',
Expand Down
14 changes: 2 additions & 12 deletions src/models/gtfs/booking-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,7 @@ export const bookingRules = {
},
{
name: 'prior_notice_last_time',
type: 'text',
},
{
name: 'prior_notice_last_timestamp',
type: 'integer',
index: true,
type: 'time',
},
{
name: 'prior_notice_start_day',
Expand All @@ -46,12 +41,7 @@ export const bookingRules = {
},
{
name: 'prior_notice_start_time',
type: 'text',
},
{
name: 'prior_notice_start_timestamp',
type: 'integer',
index: true,
type: 'time',
},
{
name: 'prior_notice_service_id',
Expand Down
12 changes: 2 additions & 10 deletions src/models/gtfs/frequencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,15 @@ export const frequencies = {
},
{
name: 'start_time',
type: 'text',
type: 'time',
required: true,
primary: true,
},
{
name: 'start_timestamp',
type: 'integer',
},
{
name: 'end_time',
type: 'text',
type: 'time',
required: true,
},
{
name: 'end_timestamp',
type: 'integer',
},
{
name: 'headway_secs',
type: 'integer',
Expand Down
Loading

0 comments on commit 6c2665b

Please sign in to comment.