Skip to content

Commit d5d6265

Browse files
authored
Merge pull request #117 from Mytherin/timestamps
Add support for reading unix epochs as timestamps
2 parents 3158619 + 8c5248d commit d5d6265

File tree

5 files changed

+74
-9
lines changed

5 files changed

+74
-9
lines changed

.github/workflows/MainDistributionPipeline.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ concurrency:
1414
jobs:
1515
duckdb-stable-build:
1616
name: Build extension binaries
17-
uses: duckdb/extension-ci-tools/.github/workflows/_extension_distribution.yml@main
17+
uses: duckdb/extension-ci-tools/.github/workflows/_extension_distribution.yml@v1.1.0
1818
with:
1919
duckdb_version: main
2020
extension_name: sqlite_scanner
2121

2222
duckdb-stable-deploy:
2323
name: Deploy extension binaries
2424
needs: duckdb-stable-build
25-
uses: duckdb/extension-ci-tools/.github/workflows/_extension_deploy.yml@main
25+
uses: duckdb/extension-ci-tools/.github/workflows/_extension_deploy.yml@v1.1.0
2626
secrets: inherit
2727
with:
2828
duckdb_version: main

data/db/unix_timestamp.db

16 KB
Binary file not shown.

src/sqlite_scanner.cpp

+45-7
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
#include "duckdb/main/client_context.hpp"
1313
#include "duckdb/main/config.hpp"
1414
#include "duckdb/storage/storage_extension.hpp"
15-
15+
#include "duckdb/common/operator/cast_operators.hpp"
1616
#include <cmath>
1717

1818
namespace duckdb {
@@ -204,6 +204,15 @@ static unique_ptr<GlobalTableFunctionState> SqliteInitGlobalState(ClientContext
204204
return std::move(result);
205205
}
206206

207+
static timestamp_t ConvertTimestampInteger(sqlite3_value *val) {
208+
return Timestamp::FromEpochSeconds(sqlite3_value_int64(val));
209+
}
210+
211+
static timestamp_t ConvertTimestampFloat(sqlite3_value *val) {
212+
int64_t timestamp_micros = Cast::Operation<double, int64_t>(sqlite3_value_double(val) * 1000000.0);
213+
return Timestamp::FromEpochMicroSeconds(timestamp_micros);
214+
}
215+
207216
static void SqliteScan(ClientContext &context, TableFunctionInput &data, DataChunk &output) {
208217
auto &state = data.local_state->Cast<SqliteLocalState>();
209218
auto &gstate = data.global_state->Cast<SqliteGlobalState>();
@@ -255,14 +264,43 @@ static void SqliteScan(ClientContext &context, TableFunctionInput &data, DataChu
255264
out_vec, (const char *)sqlite3_value_text(val), sqlite3_value_bytes(val));
256265
break;
257266
case LogicalTypeId::DATE:
258-
stmt.CheckTypeMatches(bind_data, val, sqlite_column_type, SQLITE_TEXT, col_idx);
259-
FlatVector::GetData<date_t>(out_vec)[out_idx] =
260-
Date::FromCString((const char *)sqlite3_value_text(val), sqlite3_value_bytes(val));
267+
if (sqlite_column_type == SQLITE_INTEGER) {
268+
// unix timestamp
269+
FlatVector::GetData<date_t>(out_vec)[out_idx] =
270+
Timestamp::GetDate(ConvertTimestampInteger(val));
271+
} else if (sqlite_column_type == SQLITE_FLOAT) {
272+
FlatVector::GetData<date_t>(out_vec)[out_idx] = Timestamp::GetDate(ConvertTimestampFloat(val));
273+
} else if (sqlite_column_type == SQLITE_TEXT) {
274+
FlatVector::GetData<date_t>(out_vec)[out_idx] =
275+
Date::FromCString((const char *)sqlite3_value_text(val), sqlite3_value_bytes(val));
276+
} else {
277+
throw NotImplementedException(
278+
"Unimplemented SQLite type for column of type DATE\n* SET sqlite_all_varchar=true to "
279+
"load all columns as VARCHAR and skip type conversions");
280+
}
261281
break;
262282
case LogicalTypeId::TIMESTAMP:
263-
stmt.CheckTypeMatches(bind_data, val, sqlite_column_type, SQLITE_TEXT, col_idx);
264-
FlatVector::GetData<timestamp_t>(out_vec)[out_idx] =
265-
Timestamp::FromCString((const char *)sqlite3_value_text(val), sqlite3_value_bytes(val));
283+
// SQLite does not have a timestamp type - but it has "conventions"
284+
// See https://www.sqlite.org/lang_datefunc.html
285+
// The conventions are:
286+
// A text string that is an ISO 8601 date/time value
287+
// The number of days including fractional days since -4713-11-24 12:00:00
288+
// The number of seconds including fractional seconds since 1970-01-01 00:00:00
289+
// for now we only support ISO-8601 and unix timestamps
290+
if (sqlite_column_type == SQLITE_INTEGER) {
291+
// unix timestamp
292+
FlatVector::GetData<timestamp_t>(out_vec)[out_idx] = ConvertTimestampInteger(val);
293+
} else if (sqlite_column_type == SQLITE_FLOAT) {
294+
FlatVector::GetData<timestamp_t>(out_vec)[out_idx] = ConvertTimestampFloat(val);
295+
} else if (sqlite_column_type == SQLITE_TEXT) {
296+
// ISO-8601
297+
FlatVector::GetData<timestamp_t>(out_vec)[out_idx] =
298+
Timestamp::FromCString((const char *)sqlite3_value_text(val), sqlite3_value_bytes(val));
299+
} else {
300+
throw NotImplementedException(
301+
"Unimplemented SQLite type for column of type TIMESTAMP\n* SET sqlite_all_varchar=true to "
302+
"load all columns as VARCHAR and skip type conversions");
303+
}
266304
break;
267305
case LogicalTypeId::BLOB:
268306
FlatVector::GetData<string_t>(out_vec)[out_idx] = StringVector::AddStringOrBlob(

src/sqlite_stmt.cpp

+2
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ void SQLiteStatement::CheckTypeMatches(const SqliteBindData &bind_data, sqlite3_
6969
auto message = "Invalid type in column \"" + column_name + "\": column was declared as " +
7070
SQLiteUtils::TypeToString(expected_type) + ", found \"" + value_as_text + "\" of type \"" +
7171
SQLiteUtils::TypeToString(sqlite_column_type) + "\" instead.";
72+
message += "\n* SET sqlite_all_varchar=true to load all columns as VARCHAR and skip type conversions";
7273
throw Exception(ExceptionType::MISMATCH_TYPE, message);
7374
}
7475
}
@@ -79,6 +80,7 @@ void SQLiteStatement::CheckTypeIsFloatOrInteger(sqlite3_value *val, int sqlite_c
7980
auto value_as_text = string((const char *)sqlite3_value_text(val));
8081
auto message = "Invalid type in column \"" + column_name + "\": expected float or integer, found \"" +
8182
value_as_text + "\" of type \"" + SQLiteUtils::TypeToString(sqlite_column_type) + "\" instead.";
83+
message += "\n* SET sqlite_all_varchar=true to load all columns as VARCHAR and skip type conversions";
8284
throw Exception(ExceptionType::MISMATCH_TYPE, message);
8385
}
8486
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# name: test/sql/storage/attach_unix_timestamp.test
2+
# description:
3+
# group: [sqlite_storage]
4+
5+
require sqlite_scanner
6+
7+
statement ok
8+
ATTACH 'data/db/unix_timestamp.db' AS s (TYPE SQLITE, READONLY)
9+
10+
query I
11+
SELECT * FROM s.timestamp
12+
----
13+
2024-09-23 08:06:20
14+
2024-09-23 08:06:22
15+
16+
query I
17+
SELECT * FROM s.timestamp_fractional
18+
----
19+
2024-09-23 08:06:20.5
20+
2024-09-23 08:06:22.123456
21+
22+
statement error
23+
SELECT * FROM s.timestamp_out_of_range
24+
----
25+
out of range

0 commit comments

Comments
 (0)